сделал лабу
This commit is contained in:
@@ -72,6 +72,7 @@ dependencies {
|
||||
implementation 'org.springframework.boot:spring-boot-starter-validation'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
|
||||
implementation 'org.liquibase:liquibase-core:4.30.0'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
|
||||
|
||||
runtimeOnly 'com.h2database:h2'
|
||||
|
||||
|
||||
Binary file not shown.
@@ -1,12 +1,4 @@
|
||||
2025-11-14 13:30:14.250845+04:00 jdbc[3]: exception
|
||||
org.h2.jdbc.JdbcSQLSyntaxErrorException: Таблица "DATABASECHANGELOGLOCK" не найдена
|
||||
Table "DATABASECHANGELOGLOCK" not found; SQL statement:
|
||||
SELECT COUNT(*) FROM PUBLIC.DATABASECHANGELOGLOCK [42102-224]
|
||||
2025-11-14 14:12:48.415553+04:00 jdbc[3]: exception
|
||||
org.h2.jdbc.JdbcSQLSyntaxErrorException: Таблица "DIRECTORS" уже существует
|
||||
Table "DIRECTORS" already exists; SQL statement:
|
||||
CREATE TABLE PUBLIC.DIRECTORS (ID BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, NAME VARCHAR(255) NOT NULL, CONSTRAINT CONSTRAINT_6 PRIMARY KEY (ID)) [42101-224]
|
||||
2025-11-14 14:17:18.481717+04:00 jdbc[3]: exception
|
||||
2025-11-14 20:25:48.248270+04:00 jdbc[3]: exception
|
||||
org.h2.jdbc.JdbcSQLSyntaxErrorException: Таблица "DATABASECHANGELOGLOCK" не найдена
|
||||
Table "DATABASECHANGELOGLOCK" not found; SQL statement:
|
||||
SELECT COUNT(*) FROM PUBLIC.DATABASECHANGELOGLOCK [42102-224]
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
package ru.ulstu.is.server.api;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import ru.ulstu.is.server.dto.MovieRq;
|
||||
import ru.ulstu.is.server.dto.MovieRs;
|
||||
import ru.ulstu.is.server.service.MovieService;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import ru.ulstu.is.server.dto.PageHelper;
|
||||
import ru.ulstu.is.server.dto.PageRs;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/1.0/movies")
|
||||
public class MovieController {
|
||||
|
||||
private final MovieService service;
|
||||
public MovieController(MovieService service) { this.service = service; }
|
||||
|
||||
@GetMapping
|
||||
public List<MovieRs> all() { return service.getAll(); }
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public MovieRs one(@PathVariable Long id) { return service.get(id); }
|
||||
|
||||
@PostMapping
|
||||
@ResponseStatus(HttpStatus.CREATED)
|
||||
public MovieRs create(@RequestBody @Valid MovieRq rq) { return service.create(rq); }
|
||||
|
||||
@PutMapping("/{id}")
|
||||
public MovieRs update(@PathVariable Long id, @RequestBody @Valid MovieRq rq) { return service.update(id, rq); }
|
||||
|
||||
@GetMapping("/page")
|
||||
public PageRs<MovieRs> getPage(
|
||||
@RequestParam(defaultValue = "1") @Min(1) int page,
|
||||
@RequestParam(defaultValue = "12") @Min(1) int size) {
|
||||
Pageable pageable = PageHelper.toPageable(page, size);
|
||||
return service.getPage(pageable);
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||
public void delete(@PathVariable Long id) { service.delete(id); }
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package ru.ulstu.is.server.api;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import ru.ulstu.is.server.dto.SubscriptionRq;
|
||||
import ru.ulstu.is.server.dto.SubscriptionRs;
|
||||
import ru.ulstu.is.server.service.SubscriptionService;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/1.0/subscriptions")
|
||||
public class SubscriptionController {
|
||||
private final SubscriptionService service;
|
||||
public SubscriptionController(SubscriptionService service) { this.service = service; }
|
||||
|
||||
@GetMapping public List<SubscriptionRs> all() { return service.getAll(); }
|
||||
@GetMapping("/{id}") public SubscriptionRs one(@PathVariable Long id) { return service.get(id); }
|
||||
|
||||
@PostMapping
|
||||
@ResponseStatus(HttpStatus.CREATED)
|
||||
public SubscriptionRs create(@RequestBody @Valid SubscriptionRq rq) { return service.create(rq); }
|
||||
|
||||
@PutMapping("/{id}")
|
||||
public SubscriptionRs update(@PathVariable Long id, @RequestBody @Valid SubscriptionRq rq) {
|
||||
return service.update(id, rq);
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||
public void delete(@PathVariable Long id) { service.delete(id); }
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
package ru.ulstu.is.server.mvc;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.validation.BindingResult;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import ru.ulstu.is.server.dto.*;
|
||||
import ru.ulstu.is.server.service.DirectorService;
|
||||
import ru.ulstu.is.server.service.GenreService;
|
||||
import ru.ulstu.is.server.service.MovieService;
|
||||
import ru.ulstu.is.server.session.MovieSearchSession;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/movies")
|
||||
public class MovieMvcController {
|
||||
|
||||
private final MovieService movieService;
|
||||
private final GenreService genreService;
|
||||
private final DirectorService directorService;
|
||||
private final MovieSearchSession movieSearchSession;
|
||||
|
||||
public MovieMvcController(MovieService movieService,
|
||||
GenreService genreService,
|
||||
DirectorService directorService,
|
||||
MovieSearchSession movieSearchSession) {
|
||||
this.movieService = movieService;
|
||||
this.genreService = genreService;
|
||||
this.directorService = directorService;
|
||||
this.movieSearchSession = movieSearchSession;
|
||||
}
|
||||
|
||||
|
||||
@GetMapping
|
||||
public String listMovies(
|
||||
@RequestParam(value = "page", required = false) Integer page,
|
||||
@RequestParam(value = "size", required = false) Integer size,
|
||||
@RequestParam(required = false) String title,
|
||||
@RequestParam(required = false) Long genreId,
|
||||
@RequestParam(required = false) Float minGrade,
|
||||
Model model
|
||||
) {
|
||||
int pageNumber = (page == null || page < 1) ? 1 : page;
|
||||
int pageSize = (size == null || size < 1) ? 12 : size;
|
||||
|
||||
boolean filtersPassedInRequest =
|
||||
(title != null && !title.isBlank())
|
||||
|| genreId != null
|
||||
|| minGrade != null;
|
||||
|
||||
if (!filtersPassedInRequest) {
|
||||
if (movieSearchSession.getLastTitle() != null) {
|
||||
title = movieSearchSession.getLastTitle();
|
||||
}
|
||||
if (movieSearchSession.getLastGenreId() != null) {
|
||||
genreId = movieSearchSession.getLastGenreId();
|
||||
}
|
||||
if (movieSearchSession.getLastMinGrade() != null) {
|
||||
minGrade = movieSearchSession.getLastMinGrade();
|
||||
}
|
||||
if (movieSearchSession.getLastPageSize() != null) {
|
||||
pageSize = movieSearchSession.getLastPageSize();
|
||||
}
|
||||
} else {
|
||||
movieSearchSession.setLastTitle(title);
|
||||
movieSearchSession.setLastGenreId(genreId);
|
||||
movieSearchSession.setLastMinGrade(minGrade);
|
||||
}
|
||||
|
||||
movieSearchSession.setLastPageSize(pageSize);
|
||||
|
||||
boolean hasFilters =
|
||||
(title != null && !title.isBlank())
|
||||
|| genreId != null
|
||||
|| minGrade != null;
|
||||
|
||||
PageRs<MovieRs> pageRs;
|
||||
if (hasFilters) {
|
||||
pageRs = movieService.search(title, genreId, minGrade, pageNumber, pageSize);
|
||||
} else {
|
||||
Pageable pageable = PageHelper.toPageable(pageNumber, pageSize);
|
||||
pageRs = movieService.getPage(pageable);
|
||||
}
|
||||
|
||||
prepareReferenceData(model);
|
||||
|
||||
model.addAttribute("page", pageRs);
|
||||
model.addAttribute("currentPage", pageRs.currentPage());
|
||||
model.addAttribute("pageSize", pageRs.currentSize());
|
||||
|
||||
model.addAttribute("titleFilter", title);
|
||||
model.addAttribute("genreIdFilter", genreId);
|
||||
model.addAttribute("minGradeFilter", minGrade);
|
||||
|
||||
return "movies/list";
|
||||
}
|
||||
|
||||
|
||||
@GetMapping("/new")
|
||||
public String showCreateForm(Model model) {
|
||||
MovieRq movieRq = new MovieRq();
|
||||
movieRq.setGenre(new GenreRefRq());
|
||||
movieRq.setDirector(new DirectorRefRq());
|
||||
|
||||
prepareReferenceData(model);
|
||||
model.addAttribute("movie", movieRq);
|
||||
model.addAttribute("isEdit", false);
|
||||
return "movies/form";
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public String createMovie(
|
||||
@Valid @ModelAttribute("movie") MovieRq movieRq,
|
||||
BindingResult bindingResult,
|
||||
Model model
|
||||
) {
|
||||
if (bindingResult.hasErrors()) {
|
||||
prepareReferenceData(model);
|
||||
model.addAttribute("isEdit", false);
|
||||
return "movies/form";
|
||||
}
|
||||
|
||||
movieService.create(movieRq);
|
||||
return "redirect:/movies";
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/edit")
|
||||
public String showEditForm(@PathVariable Long id, Model model) {
|
||||
MovieRs movie = movieService.get(id);
|
||||
|
||||
MovieRq movieRq = new MovieRq();
|
||||
movieRq.setTitle(movie.getTitle());
|
||||
movieRq.setImage(movie.getImage());
|
||||
movieRq.setGrade(movie.getGrade());
|
||||
|
||||
GenreRefRq genreRef = new GenreRefRq();
|
||||
if (movie.getGenre() != null) {
|
||||
genreRef.setId(movie.getGenre().getId() != null
|
||||
? movie.getGenre().getId().toString()
|
||||
: null);
|
||||
genreRef.setName(movie.getGenre().getName());
|
||||
}
|
||||
movieRq.setGenre(genreRef);
|
||||
|
||||
DirectorRefRq directorRef = new DirectorRefRq();
|
||||
if (movie.getDirector() != null) {
|
||||
directorRef.setId(movie.getDirector().getId() != null
|
||||
? movie.getDirector().getId().toString()
|
||||
: null);
|
||||
directorRef.setName(movie.getDirector().getName());
|
||||
}
|
||||
movieRq.setDirector(directorRef);
|
||||
|
||||
prepareReferenceData(model);
|
||||
model.addAttribute("movieId", movie.getId());
|
||||
model.addAttribute("movie", movieRq);
|
||||
model.addAttribute("isEdit", true);
|
||||
return "movies/form";
|
||||
}
|
||||
|
||||
@PostMapping("/{id}")
|
||||
public String updateMovie(
|
||||
@PathVariable Long id,
|
||||
@Valid @ModelAttribute("movie") MovieRq movieRq,
|
||||
BindingResult bindingResult,
|
||||
Model model
|
||||
) {
|
||||
if (bindingResult.hasErrors()) {
|
||||
prepareReferenceData(model);
|
||||
model.addAttribute("movieId", id);
|
||||
model.addAttribute("isEdit", true);
|
||||
return "movies/form";
|
||||
}
|
||||
|
||||
movieService.update(id, movieRq);
|
||||
return "redirect:/movies";
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/delete")
|
||||
public String deleteMovie(@PathVariable Long id) {
|
||||
movieService.delete(id);
|
||||
return "redirect:/movies";
|
||||
}
|
||||
|
||||
private void prepareReferenceData(Model model) {
|
||||
List<GenreRs> genres = genreService.getAll();
|
||||
List<DirectorRs> directors = directorService.getAll();
|
||||
model.addAttribute("genres", genres);
|
||||
model.addAttribute("directors", directors);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package ru.ulstu.is.server.mvc;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.validation.BindingResult;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import ru.ulstu.is.server.dto.PageHelper;
|
||||
import ru.ulstu.is.server.dto.PageRs;
|
||||
import ru.ulstu.is.server.dto.SubscriptionRq;
|
||||
import ru.ulstu.is.server.dto.SubscriptionRs;
|
||||
import ru.ulstu.is.server.service.SubscriptionService;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/subscriptions")
|
||||
public class SubscriptionMvcController {
|
||||
|
||||
private final SubscriptionService subscriptionService;
|
||||
|
||||
public SubscriptionMvcController(SubscriptionService subscriptionService) {
|
||||
this.subscriptionService = subscriptionService;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public String listSubscriptions(
|
||||
@RequestParam(defaultValue = "1") @Min(1) int page,
|
||||
@RequestParam(defaultValue = "10") @Min(1) int size,
|
||||
Model model
|
||||
) {
|
||||
Pageable pageable = PageHelper.toPageable(page, size);
|
||||
PageRs<SubscriptionRs> pageRs = subscriptionService.getPage(pageable);
|
||||
|
||||
model.addAttribute("page", pageRs);
|
||||
model.addAttribute("currentPage", pageRs.currentPage());
|
||||
model.addAttribute("pageSize", pageRs.currentSize());
|
||||
|
||||
return "subscriptions/list";
|
||||
}
|
||||
|
||||
@GetMapping("/new")
|
||||
public String showCreateForm(Model model) {
|
||||
model.addAttribute("subscription", new SubscriptionRq());
|
||||
model.addAttribute("isEdit", false);
|
||||
return "subscriptions/form";
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public String createSubscription(
|
||||
@Valid @ModelAttribute("subscription") SubscriptionRq subscriptionRq,
|
||||
BindingResult bindingResult,
|
||||
Model model
|
||||
) {
|
||||
if (bindingResult.hasErrors()) {
|
||||
model.addAttribute("isEdit", false);
|
||||
return "subscriptions/form";
|
||||
}
|
||||
|
||||
subscriptionService.create(subscriptionRq);
|
||||
return "redirect:/subscriptions";
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/edit")
|
||||
public String showEditForm(@PathVariable Long id, Model model) {
|
||||
SubscriptionRs subscription = subscriptionService.get(id);
|
||||
|
||||
SubscriptionRq rq = new SubscriptionRq();
|
||||
rq.setLevel(subscription.getLevel());
|
||||
rq.setPrice(subscription.getPrice());
|
||||
|
||||
model.addAttribute("subscriptionId", subscription.getId());
|
||||
model.addAttribute("subscription", rq);
|
||||
model.addAttribute("isEdit", true);
|
||||
return "subscriptions/form";
|
||||
}
|
||||
|
||||
@PostMapping("/{id}")
|
||||
public String updateSubscription(
|
||||
@PathVariable Long id,
|
||||
@Valid @ModelAttribute("subscription") SubscriptionRq subscriptionRq,
|
||||
BindingResult bindingResult,
|
||||
Model model
|
||||
) {
|
||||
if (bindingResult.hasErrors()) {
|
||||
model.addAttribute("subscriptionId", id);
|
||||
model.addAttribute("isEdit", true);
|
||||
return "subscriptions/form";
|
||||
}
|
||||
|
||||
subscriptionService.update(id, subscriptionRq);
|
||||
return "redirect:/subscriptions";
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/delete")
|
||||
public String deleteSubscription(@PathVariable Long id) {
|
||||
subscriptionService.delete(id);
|
||||
return "redirect:/subscriptions";
|
||||
}
|
||||
}
|
||||
@@ -49,6 +49,72 @@ public class MovieService {
|
||||
return PageRs.from(movies.findAll(pageable), mapper::toRs);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public PageRs<MovieRs> search(String title, Long genreId, Float minGrade, int page, int size) {
|
||||
List<MovieRs> all = getAll();
|
||||
|
||||
var stream = all.stream();
|
||||
|
||||
if (title != null && !title.isBlank()) {
|
||||
String lower = title.toLowerCase();
|
||||
stream = stream.filter(m ->
|
||||
m.getTitle() != null && m.getTitle().toLowerCase().contains(lower)
|
||||
);
|
||||
}
|
||||
|
||||
if (genreId != null) {
|
||||
stream = stream.filter(m ->
|
||||
m.getGenre() != null
|
||||
&& m.getGenre().getId() != null
|
||||
&& m.getGenre().getId().equals(genreId)
|
||||
);
|
||||
}
|
||||
|
||||
if (minGrade != null) {
|
||||
stream = stream.filter(m -> m.getGrade() >= minGrade);
|
||||
}
|
||||
|
||||
List<MovieRs> filtered = stream.toList();
|
||||
int totalItems = filtered.size();
|
||||
|
||||
if (size <= 0) {
|
||||
size = 10;
|
||||
}
|
||||
|
||||
int totalPages = totalItems == 0
|
||||
? 1
|
||||
: (int) Math.ceil(totalItems / (double) size);
|
||||
|
||||
int currentPage = page < 1 ? 1 : Math.min(page, totalPages);
|
||||
|
||||
int fromIndex = (currentPage - 1) * size;
|
||||
if (fromIndex > totalItems) {
|
||||
fromIndex = totalItems;
|
||||
}
|
||||
int toIndex = Math.min(fromIndex + size, totalItems);
|
||||
|
||||
List<MovieRs> pageItems = filtered.subList(fromIndex, toIndex);
|
||||
|
||||
boolean isFirst = currentPage == 1;
|
||||
boolean isLast = currentPage >= totalPages;
|
||||
boolean hasNext = currentPage < totalPages;
|
||||
boolean hasPrevious = currentPage > 1;
|
||||
|
||||
return new PageRs<>(
|
||||
pageItems,
|
||||
pageItems.size(),
|
||||
currentPage,
|
||||
size,
|
||||
totalPages,
|
||||
totalItems,
|
||||
isFirst,
|
||||
isLast,
|
||||
hasNext,
|
||||
hasPrevious
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public MovieRs get(Long id) {
|
||||
Movie m = movies.findById(id).orElseThrow(() -> new NotFoundException(ru.ulstu.is.server.entity.Movie.class, "movie " + id + " not found"));
|
||||
|
||||
@@ -8,6 +8,9 @@ import ru.ulstu.is.server.entity.Subscription;
|
||||
import ru.ulstu.is.server.mapper.SubscriptionMapper;
|
||||
import ru.ulstu.is.server.repository.SubscriptionRepository;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import ru.ulstu.is.server.dto.PageHelper;
|
||||
import ru.ulstu.is.server.dto.PageRs;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -37,6 +40,28 @@ public class SubscriptionService {
|
||||
return mapper.toRs(s);
|
||||
}
|
||||
|
||||
public PageRs<SubscriptionRs> getPage(Pageable pageable) {
|
||||
var page = repo.findAll(pageable);
|
||||
|
||||
var items = page.getContent()
|
||||
.stream()
|
||||
.map(mapper::toRs)
|
||||
.toList();
|
||||
|
||||
return new PageRs<>(
|
||||
items,
|
||||
page.getNumberOfElements(),
|
||||
page.getNumber() + 1,
|
||||
page.getSize(),
|
||||
page.getTotalPages(),
|
||||
page.getTotalElements(),
|
||||
page.isFirst(),
|
||||
page.isLast(),
|
||||
page.hasNext(),
|
||||
page.hasPrevious()
|
||||
);
|
||||
}
|
||||
|
||||
public SubscriptionRs update(Long id, SubscriptionRq rq) {
|
||||
Subscription s = getEntity(id);
|
||||
mapper.updateEntity(s, rq);
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package ru.ulstu.is.server.session;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.context.annotation.SessionScope;
|
||||
|
||||
@Component
|
||||
@SessionScope
|
||||
public class MovieSearchSession {
|
||||
|
||||
private String lastTitle;
|
||||
private Long lastGenreId;
|
||||
private Float lastMinGrade;
|
||||
private Integer lastPageSize;
|
||||
|
||||
public String getLastTitle() {
|
||||
return lastTitle;
|
||||
}
|
||||
|
||||
public void setLastTitle(String lastTitle) {
|
||||
this.lastTitle = lastTitle;
|
||||
}
|
||||
|
||||
public Long getLastGenreId() {
|
||||
return lastGenreId;
|
||||
}
|
||||
|
||||
public void setLastGenreId(Long lastGenreId) {
|
||||
this.lastGenreId = lastGenreId;
|
||||
}
|
||||
|
||||
public Float getLastMinGrade() {
|
||||
return lastMinGrade;
|
||||
}
|
||||
|
||||
public void setLastMinGrade(Float lastMinGrade) {
|
||||
this.lastMinGrade = lastMinGrade;
|
||||
}
|
||||
|
||||
public Integer getLastPageSize() {
|
||||
return lastPageSize;
|
||||
}
|
||||
|
||||
public void setLastPageSize(Integer lastPageSize) {
|
||||
this.lastPageSize = lastPageSize;
|
||||
}
|
||||
}
|
||||
@@ -16,4 +16,4 @@ spring:
|
||||
format_sql: true
|
||||
logging:
|
||||
level:
|
||||
org.springdoc: DEBUG
|
||||
org.springdoc: WARN
|
||||
|
||||
314
backend/src/main/resources/static/css/style.css
Normal file
314
backend/src/main/resources/static/css/style.css
Normal file
@@ -0,0 +1,314 @@
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
background-color: #181818;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
nav a {
|
||||
color: #fff;
|
||||
margin: 10px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
nav a:hover {
|
||||
color: #FF0033;
|
||||
}
|
||||
|
||||
.movie-list {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
width: 500px;
|
||||
flex-wrap: wrap;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.movie-list a {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
|
||||
.movie-list img {
|
||||
width: 150px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
table {
|
||||
margin: auto;
|
||||
background-color: #282828;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.user_photo {
|
||||
height: 130px;
|
||||
width: 130px;
|
||||
border-radius: 50px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
#changeInfo {
|
||||
font-size: 20px;
|
||||
padding: 10px;
|
||||
background-color: gray;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#current_page {
|
||||
color: red;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
background-color: #181818;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 20px;
|
||||
background-color: #202020;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.logo img {
|
||||
height: 40px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
nav ul {
|
||||
display: flex;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
nav ul li {
|
||||
position: relative;
|
||||
margin: 0 15px;
|
||||
}
|
||||
|
||||
nav ul li a {
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
display: none;
|
||||
position: absolute;
|
||||
background-color: #333;
|
||||
list-style: none;
|
||||
top: 100%;
|
||||
left: -40px;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.dropdown-menu li {
|
||||
padding: 5px 10px;
|
||||
}
|
||||
|
||||
.dropdown-movies:hover .dropdown-menu {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.user-actions a {
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 20px;
|
||||
background-color: #202020;
|
||||
}
|
||||
|
||||
.social-links img {
|
||||
height: 30px;
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
main {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
footer {
|
||||
background-color: #202020;
|
||||
padding: 10px 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.form-container {
|
||||
width: 300px;
|
||||
margin: 50px auto;
|
||||
padding: 20px;
|
||||
background-color: #282828;
|
||||
border-radius: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.form-container h2 {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-container input {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
margin: 10px 0;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.form-container button {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
background-color: red;
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.form-container button:hover {
|
||||
background-color: darkred;
|
||||
}
|
||||
|
||||
.form-container a {
|
||||
color: red;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.user-menu {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.account-btn {
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.user-menu:hover .dropdown {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
display: none;
|
||||
position: absolute;
|
||||
background-color: #333;
|
||||
list-style: none;
|
||||
padding: 5px;
|
||||
left: -22px;
|
||||
margin: 0;
|
||||
border-radius: 5px;
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
.dropdown li {
|
||||
padding: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.dropdown li a {
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.dropdown li a:hover {
|
||||
background-color: red;
|
||||
}
|
||||
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
header {
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
nav ul {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.movie-list {
|
||||
max-width: 90%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
header {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
nav ul {
|
||||
flex-direction: column;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.movie-list {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
footer {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.user-menu {
|
||||
display: block;
|
||||
position: static;
|
||||
background-color: transparent;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.user-menu li {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.dropdown:hover .user-menu {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.side-menu {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.movie-list img {
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
footer {
|
||||
padding: 5px;
|
||||
}
|
||||
}
|
||||
BIN
backend/src/main/resources/static/img/logo.png
Normal file
BIN
backend/src/main/resources/static/img/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 216 KiB |
16
backend/src/main/resources/templates/error.html
Normal file
16
backend/src/main/resources/templates/error.html
Normal file
@@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org" lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Ошибка</title>
|
||||
</head>
|
||||
<body style="font-family: system-ui, sans-serif;">
|
||||
<h1>Что-то пошло не так</h1>
|
||||
|
||||
<p><strong>Путь:</strong> <span th:text="${path}">/path</span></p>
|
||||
<p><strong>Статус:</strong> <span th:text="${status}">500</span></p>
|
||||
<p><strong>Ошибка:</strong> <span th:text="${error}">Error</span></p>
|
||||
<p><strong>Сообщение:</strong> <span th:text="${message}">Message</span></p>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
79
backend/src/main/resources/templates/layout.html
Normal file
79
backend/src/main/resources/templates/layout.html
Normal file
@@ -0,0 +1,79 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org" lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Онлайн-кинотеатр</title>
|
||||
|
||||
<!-- Bootstrap + иконки + твой CSS, как в фронте -->
|
||||
<link rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css">
|
||||
<link rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
|
||||
<link rel="stylesheet" th:href="@{/css/style.css}">
|
||||
</head>
|
||||
<body class="bg-dark text-light">
|
||||
|
||||
<!-- ====== HEADER ====== -->
|
||||
<header th:fragment="siteHeader">
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark border-bottom border-secondary">
|
||||
<div class="container">
|
||||
|
||||
<!-- Лого -->
|
||||
<a class="navbar-brand d-flex align-items-center gap-2" th:href="@{/}">
|
||||
<img th:src="@{/img/logo.png}" alt="Логотип" width="40"/>
|
||||
<!-- Если хочешь – можно убрать текст -->
|
||||
<span class="fw-semibold">Cinema</span>
|
||||
</a>
|
||||
|
||||
<button class="navbar-toggler"
|
||||
type="button"
|
||||
data-bs-toggle="collapse"
|
||||
data-bs-target="#navbarNav"
|
||||
aria-controls="navbarNav"
|
||||
aria-expanded="false"
|
||||
aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<!-- Левые ссылки -->
|
||||
<ul class="navbar-nav me-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" th:href="@{/}">Главная</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" th:href="@{/movies}">Фильмы</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" th:href="@{/subscriptions}">Подписки</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<!-- Правая часть (пока статично, без useAuth) -->
|
||||
<ul class="navbar-nav ms-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" th:href="@{/login}">Войти</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" th:href="@{/register}">Регистрация</a>
|
||||
</li>
|
||||
<!-- потом можем сделать дропдаун "Профиль" по данным из сессии -->
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<!-- ====== FOOTER ====== -->
|
||||
<footer th:fragment="siteFooter" class="bg-dark text-light mt-4 py-3 border-top border-secondary">
|
||||
<div class="container text-center small">
|
||||
<p class="mb-1">Контакты: cinemaSupport@gmail.com</p>
|
||||
<p class="mb-1">Адрес: г. Москва, ул. Киношная, д. 5</p>
|
||||
<p class="mb-0">Часы работы: 10:00 – 22:00</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- Bootstrap JS (для бургер-меню и дропдаунов) -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
101
backend/src/main/resources/templates/movies/form.html
Normal file
101
backend/src/main/resources/templates/movies/form.html
Normal file
@@ -0,0 +1,101 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org" lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title th:text="${isEdit} ? 'Редактирование фильма' : 'Новый фильм'">Фильм</title>
|
||||
|
||||
<link rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css">
|
||||
<link rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
|
||||
<link rel="stylesheet" th:href="@{/css/style.css}">
|
||||
</head>
|
||||
<body class="bg-dark text-light">
|
||||
|
||||
<div th:insert="layout :: siteHeader"></div>
|
||||
|
||||
<main class="container my-4">
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h1 class="mb-0"
|
||||
th:text="${isEdit} ? 'Редактирование фильма' : 'Новый фильм'">
|
||||
Фильм
|
||||
</h1>
|
||||
|
||||
<a th:href="@{/movies}" class="btn btn-outline-light">
|
||||
<i class="bi bi-arrow-left"></i>
|
||||
Назад к списку
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="card bg-dark border-secondary shadow-sm">
|
||||
<div class="card-body">
|
||||
<form th:object="${movie}"
|
||||
th:action="${isEdit} ? @{|/movies/${movieId}|} : @{/movies}"
|
||||
method="post"
|
||||
class="row g-3">
|
||||
|
||||
<!-- Название -->
|
||||
<div class="col-12">
|
||||
<label class="form-label text-light">Название</label>
|
||||
<input type="text" th:field="*{title}" class="form-control"/>
|
||||
<div class="text-danger small"
|
||||
th:if="${#fields.hasErrors('title')}"
|
||||
th:errors="*{title}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- URL постера -->
|
||||
<div class="col-12">
|
||||
<label class="form-label text-light">URL постера</label>
|
||||
<input type="text" th:field="*{image}" class="form-control"/>
|
||||
</div>
|
||||
|
||||
<!-- Оценка -->
|
||||
<div class="col-md-3">
|
||||
<label class="form-label text-light">Оценка</label>
|
||||
<input type="number" step="0.1" th:field="*{grade}"
|
||||
class="form-control"/>
|
||||
</div>
|
||||
|
||||
<!-- Жанр -->
|
||||
<div class="col-md-4">
|
||||
<label class="form-label text-light">Жанр</label>
|
||||
<select th:field="*{genre.id}" class="form-select">
|
||||
<option value="">-- выберите жанр --</option>
|
||||
<option th:each="g : ${genres}"
|
||||
th:value="${g.id}"
|
||||
th:text="${g.name}">
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Режиссёр -->
|
||||
<div class="col-md-5">
|
||||
<label class="form-label text-light">Режиссёр</label>
|
||||
<select th:field="*{director.id}" class="form-select">
|
||||
<option value="">-- выберите режиссёра --</option>
|
||||
<option th:each="d : ${directors}"
|
||||
th:value="${d.id}"
|
||||
th:text="${d.name}">
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Кнопка -->
|
||||
<div class="col-12 d-flex justify-content-end mt-3">
|
||||
<button type="submit" class="btn btn-primary"
|
||||
th:text="${isEdit} ? 'Сохранить' : 'Создать'">
|
||||
Сохранить
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<div th:insert="layout :: siteFooter"></div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
161
backend/src/main/resources/templates/movies/list.html
Normal file
161
backend/src/main/resources/templates/movies/list.html
Normal file
@@ -0,0 +1,161 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org" lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Фильмы</title>
|
||||
<link rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css">
|
||||
<link rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
|
||||
<link rel="stylesheet" th:href="@{/css/style.css}">
|
||||
</head>
|
||||
<body class="bg-dark text-light">
|
||||
|
||||
<div th:insert="layout :: siteHeader"></div>
|
||||
|
||||
<main class="container my-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h1 class="mb-0">Фильмы</h1>
|
||||
<a th:href="@{/movies/new}" class="btn btn-primary">
|
||||
<i class="bi bi-plus-lg me-1"></i> Добавить фильм
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<form class="card card-body mb-4 bg-secondary bg-opacity-10"
|
||||
th:action="@{/movies}" method="get">
|
||||
<div class="row g-3 align-items-end">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label text-light">Название содержит</label>
|
||||
<input type="text" name="title" class="form-control"
|
||||
th:value="${titleFilter}">
|
||||
</div>
|
||||
|
||||
<div class="col-md-3">
|
||||
<label class="form-label text-light">Жанр</label>
|
||||
<select name="genreId" class="form-select">
|
||||
<option value="">-- любой --</option>
|
||||
<option th:each="g : ${genres}"
|
||||
th:value="${g.id}"
|
||||
th:text="${g.name}"
|
||||
th:selected="${genreIdFilter != null and g.id == genreIdFilter}">
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-md-2">
|
||||
<label class="form-label text-light">Мин. оценка</label>
|
||||
<input type="number" step="0.1" name="minGrade"
|
||||
class="form-control"
|
||||
th:value="${minGradeFilter}">
|
||||
</div>
|
||||
|
||||
<div class="col-md-2">
|
||||
<label class="form-label text-light">Размер страницы</label>
|
||||
<input type="number" min="1" name="size"
|
||||
class="form-control"
|
||||
th:value="${pageSize}">
|
||||
<input type="hidden" name="page" value="1">
|
||||
</div>
|
||||
|
||||
<div class="col-md-1 d-grid">
|
||||
<button type="submit" class="btn btn-success">OK</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-12 col-sm-6 col-md-4 col-lg-3"
|
||||
th:each="movie : ${page.items}">
|
||||
<div class="card h-100 bg-dark border-secondary shadow-sm">
|
||||
<img th:if="${movie.image != null and !#strings.isEmpty(movie.image)}"
|
||||
th:src="${movie.image}"
|
||||
class="card-img-top"
|
||||
alt="Постер"
|
||||
style="object-fit: cover; height: 260px;">
|
||||
<div class="card-body d-flex flex-column">
|
||||
<h5 class="card-title" th:text="${movie.title}">Название</h5>
|
||||
|
||||
<p class="card-text text-secondary mb-1">
|
||||
Жанр –
|
||||
<span th:text="${movie.genre != null ? movie.genre.name : 'Без жанра'}">
|
||||
Жанр
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<p class="card-text text-secondary mb-1">
|
||||
Режиссёр –
|
||||
<span th:text="${movie.director != null ? movie.director.name : 'Без режиссёра'}">
|
||||
Режиссёр
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<p class="card-text text-warning mb-2">
|
||||
<i class="bi bi-star-fill me-1"></i>
|
||||
<span th:text="${movie.grade}">0</span>
|
||||
</p>
|
||||
|
||||
<div class="mt-auto d-flex justify-content-between">
|
||||
<a th:href="@{|/movies/${movie.id}/edit|}"
|
||||
class="btn btn-sm btn-outline-light">
|
||||
Редактировать
|
||||
</a>
|
||||
<a th:href="@{|/movies/${movie.id}/delete|}"
|
||||
class="btn btn-sm btn-outline-danger"
|
||||
onclick="return confirm('Удалить фильм?');">
|
||||
Удалить
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav class="mt-4">
|
||||
<ul class="pagination justify-content-center">
|
||||
<li class="page-item" th:classappend="${page.isFirst} ? ' disabled'">
|
||||
<a class="page-link"
|
||||
th:href="@{/movies(
|
||||
page=${page.currentPage - 1},
|
||||
size=${page.currentSize},
|
||||
title=${titleFilter},
|
||||
genreId=${genreIdFilter},
|
||||
minGrade=${minGradeFilter}
|
||||
)}">
|
||||
Предыдущая
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="page-item"
|
||||
th:each="p : ${#numbers.sequence(1, page.totalPages)}"
|
||||
th:classappend="${p == page.currentPage} ? ' active'">
|
||||
<a class="page-link"
|
||||
th:text="${p}"
|
||||
th:href="@{/movies(
|
||||
page=${p},
|
||||
size=${page.currentSize},
|
||||
title=${titleFilter},
|
||||
genreId=${genreIdFilter},
|
||||
minGrade=${minGradeFilter}
|
||||
)}">1</a>
|
||||
</li>
|
||||
|
||||
<li class="page-item" th:classappend="${page.isLast} ? ' disabled'">
|
||||
<a class="page-link"
|
||||
th:href="@{/movies(
|
||||
page=${page.currentPage + 1},
|
||||
size=${page.currentSize},
|
||||
title=${titleFilter},
|
||||
genreId=${genreIdFilter},
|
||||
minGrade=${minGradeFilter}
|
||||
)}">
|
||||
Следующая
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</main>
|
||||
|
||||
<div th:insert="layout :: siteFooter"></div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
91
backend/src/main/resources/templates/subscriptions/form.html
Normal file
91
backend/src/main/resources/templates/subscriptions/form.html
Normal file
@@ -0,0 +1,91 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org" lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title th:text="${isEdit} ? 'Редактирование подписки' : 'Новая подписка'">Подписка</title>
|
||||
|
||||
<link rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css">
|
||||
<link rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
|
||||
<link rel="stylesheet" th:href="@{/css/style.css}">
|
||||
</head>
|
||||
<body class="bg-dark text-light">
|
||||
|
||||
<div th:insert="layout :: siteHeader"></div>
|
||||
|
||||
<main class="container my-4">
|
||||
|
||||
<!-- Заголовок -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1 class="mb-0"
|
||||
th:text="${isEdit} ? 'Редактирование подписки' : 'Новая подписка'">
|
||||
Подписка
|
||||
</h1>
|
||||
|
||||
<a th:href="@{/subscriptions}" class="btn btn-secondary">
|
||||
<i class="bi bi-arrow-left"></i> Назад
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Форма -->
|
||||
<div class="card bg-dark border-secondary shadow-sm">
|
||||
<div class="card-body">
|
||||
|
||||
<form th:object="${subscription}"
|
||||
th:action="${isEdit} ? @{|/subscriptions/${subscriptionId}|} : @{/subscriptions}"
|
||||
method="post">
|
||||
|
||||
<!-- Уровень -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Уровень</label>
|
||||
<input type="text"
|
||||
th:field="*{level}"
|
||||
class="form-control bg-secondary text-light border-0"/>
|
||||
|
||||
<div class="text-danger mt-1"
|
||||
th:if="${#fields.hasErrors('level')}"
|
||||
th:errors="*{level}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Цена -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Цена (₽)</label>
|
||||
<input type="number"
|
||||
min="0"
|
||||
th:field="*{price}"
|
||||
class="form-control bg-secondary text-light border-0"/>
|
||||
|
||||
<div class="text-danger mt-1"
|
||||
th:if="${#fields.hasErrors('price')}"
|
||||
th:errors="*{price}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Кнопки -->
|
||||
<div class="d-flex justify-content-end gap-2">
|
||||
<a th:href="@{/subscriptions}"
|
||||
class="btn btn-outline-light">
|
||||
Отмена
|
||||
</a>
|
||||
|
||||
<button type="submit"
|
||||
class="btn btn-success"
|
||||
th:text="${isEdit} ? 'Сохранить' : 'Создать'">
|
||||
Сохранить
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
|
||||
<div th:insert="layout :: siteFooter"></div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
99
backend/src/main/resources/templates/subscriptions/list.html
Normal file
99
backend/src/main/resources/templates/subscriptions/list.html
Normal file
@@ -0,0 +1,99 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org" lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Подписки</title>
|
||||
|
||||
<link rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css">
|
||||
<link rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
|
||||
<link rel="stylesheet" th:href="@{/css/style.css}">
|
||||
</head>
|
||||
<body class="bg-dark text-light">
|
||||
|
||||
<div th:insert="layout :: siteHeader"></div>
|
||||
|
||||
<main class="container my-4">
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h1 class="mb-0">Подписки</h1>
|
||||
<a th:href="@{/subscriptions/new}" class="btn btn-primary">
|
||||
<i class="bi bi-plus-lg me-1"></i> Добавить подписку
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="card bg-dark border-secondary shadow-sm">
|
||||
<div class="card-body p-0">
|
||||
<table class="table table-dark table-hover mb-0 align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">ID</th>
|
||||
<th scope="col">Уровень</th>
|
||||
<th scope="col">Цена (₽)</th>
|
||||
<th scope="col" class="text-end">Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr th:each="sub : ${page.items}">
|
||||
<td th:text="${sub.id}">1</td>
|
||||
<td th:text="${sub.level}">Базовая</td>
|
||||
<td th:text="${sub.price}">299</td>
|
||||
<td class="text-end">
|
||||
<a th:href="@{|/subscriptions/${sub.id}/edit|}"
|
||||
class="btn btn-sm btn-outline-light me-2">
|
||||
Редактировать
|
||||
</a>
|
||||
<a th:href="@{|/subscriptions/${sub.id}/delete|}"
|
||||
class="btn btn-sm btn-outline-danger"
|
||||
onclick="return confirm('Удалить подписку?');">
|
||||
Удалить
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr th:if="${page.items.isEmpty()}">
|
||||
<td colspan="4" class="text-center text-secondary py-4">
|
||||
Подписок пока нет
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Пагинация -->
|
||||
<nav class="mt-4">
|
||||
<ul class="pagination justify-content-center">
|
||||
<li class="page-item" th:classappend="${page.isFirst} ? ' disabled'">
|
||||
<a class="page-link"
|
||||
th:href="@{/subscriptions(page=${page.currentPage - 1}, size=${page.currentSize})}">
|
||||
Предыдущая
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="page-item"
|
||||
th:each="p : ${#numbers.sequence(1, page.totalPages)}"
|
||||
th:classappend="${p == page.currentPage} ? ' active'">
|
||||
<a class="page-link"
|
||||
th:text="${p}"
|
||||
th:href="@{/subscriptions(page=${p}, size=${page.currentSize})}">
|
||||
1
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="page-item" th:classappend="${page.isLast} ? ' disabled'">
|
||||
<a class="page-link"
|
||||
th:href="@{/subscriptions(page=${page.currentPage + 1}, size=${page.currentSize})}">
|
||||
Следующая
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
</main>
|
||||
|
||||
<div th:insert="layout :: siteFooter"></div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user