Допка 5 лабы
This commit is contained in:
BIN
backend.zip
Normal file
BIN
backend.zip
Normal file
Binary file not shown.
Binary file not shown.
@@ -1,4 +0,0 @@
|
||||
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]
|
||||
|
||||
@@ -10,14 +10,4 @@ public class SpaController {
|
||||
public String index() {
|
||||
return "forward:/index.html";
|
||||
}
|
||||
|
||||
@GetMapping("/{path:^(?!api$)(?!v3$)(?!swagger-ui$).*$}")
|
||||
public String any1() {
|
||||
return "forward:/index.html";
|
||||
}
|
||||
|
||||
@GetMapping("/{path:^(?!api$)(?!v3$)(?!swagger-ui$).*$}/**")
|
||||
public String any2() {
|
||||
return "forward:/index.html";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,43 @@
|
||||
package ru.ulstu.is.server.dto;
|
||||
|
||||
import jakarta.validation.constraints.Max;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
public class SubscriptionRq {
|
||||
|
||||
@NotBlank
|
||||
private String level;
|
||||
@NotNull @Min(0)
|
||||
@NotNull
|
||||
@Min(0)
|
||||
private Integer price;
|
||||
public String getLevel() { return level; }
|
||||
public void setLevel(String level) { this.level = level; }
|
||||
public Integer getPrice() { return price; }
|
||||
public void setPrice(Integer price) { this.price = price; }
|
||||
}
|
||||
|
||||
@Min(0)
|
||||
@Max(100)
|
||||
private Integer discount;
|
||||
|
||||
public String getLevel() {
|
||||
return level;
|
||||
}
|
||||
|
||||
public void setLevel(String level) {
|
||||
this.level = level;
|
||||
}
|
||||
|
||||
public Integer getPrice() {
|
||||
return price;
|
||||
}
|
||||
|
||||
public void setPrice(Integer price) {
|
||||
this.price = price;
|
||||
}
|
||||
|
||||
public Integer getDiscount() {
|
||||
return discount;
|
||||
}
|
||||
|
||||
public void setDiscount(Integer discount) {
|
||||
this.discount = discount;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,62 @@
|
||||
package ru.ulstu.is.server.dto;
|
||||
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
public class SubscriptionRs {
|
||||
|
||||
private Long id;
|
||||
private String level;
|
||||
private Integer price;
|
||||
public SubscriptionRs() { }
|
||||
public SubscriptionRs(Long id, String level, Integer price) {
|
||||
this.id = id; this.level = level; this.price = price;
|
||||
private Integer discount;
|
||||
private Integer priceWithDiscount;
|
||||
|
||||
public SubscriptionRs() {
|
||||
}
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
public String getLevel() { return level; }
|
||||
public void setLevel(String level) { this.level = level; }
|
||||
public Integer getPrice() { return price; }
|
||||
public void setPrice(Integer price) { this.price = price; }
|
||||
}
|
||||
|
||||
public SubscriptionRs(Long id, String level, Integer price,
|
||||
Integer discount, Integer priceWithDiscount) {
|
||||
this.id = id;
|
||||
this.level = level;
|
||||
this.price = price;
|
||||
this.discount = discount;
|
||||
this.priceWithDiscount = priceWithDiscount;
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getLevel() {
|
||||
return level;
|
||||
}
|
||||
|
||||
public void setLevel(String level) {
|
||||
this.level = level;
|
||||
}
|
||||
|
||||
public Integer getPrice() {
|
||||
return price;
|
||||
}
|
||||
|
||||
public void setPrice(Integer price) {
|
||||
this.price = price;
|
||||
}
|
||||
|
||||
public Integer getDiscount() {
|
||||
return discount;
|
||||
}
|
||||
|
||||
public void setDiscount(Integer discount) {
|
||||
this.discount = discount;
|
||||
}
|
||||
|
||||
public Integer getPriceWithDiscount() {
|
||||
return priceWithDiscount;
|
||||
}
|
||||
|
||||
public void setPriceWithDiscount(Integer priceWithDiscount) {
|
||||
this.priceWithDiscount = priceWithDiscount;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
package ru.ulstu.is.server.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.OneToMany;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
@Entity
|
||||
@Table(name = "subscriptions")
|
||||
public class Subscription extends BaseEntity {
|
||||
@@ -17,7 +21,11 @@ public class Subscription extends BaseEntity {
|
||||
@OneToMany(mappedBy = "subscription", orphanRemoval = false)
|
||||
private List<User> users = new ArrayList<>();
|
||||
|
||||
public Subscription() { }
|
||||
@Column(nullable = false)
|
||||
private Integer discount = 0;
|
||||
|
||||
public Subscription() {
|
||||
}
|
||||
|
||||
public Subscription(Long id, String level, Integer price) {
|
||||
super(id);
|
||||
@@ -26,22 +34,48 @@ public class Subscription extends BaseEntity {
|
||||
}
|
||||
|
||||
public void addUser(User u) {
|
||||
if (u == null) return;
|
||||
if (u == null) {
|
||||
return;
|
||||
}
|
||||
users.add(u);
|
||||
u.setSubscription(this);
|
||||
}
|
||||
|
||||
public Integer getDiscount() {
|
||||
return discount;
|
||||
}
|
||||
|
||||
public void setDiscount(Integer discount) {
|
||||
this.discount = discount != null ? discount : 0;
|
||||
}
|
||||
|
||||
public void removeUser(User u) {
|
||||
if (u == null) return;
|
||||
if (u == null) {
|
||||
return;
|
||||
}
|
||||
users.remove(u);
|
||||
if (u.getSubscription() == this) {
|
||||
u.setSubscription(null);
|
||||
}
|
||||
}
|
||||
|
||||
public String getLevel() { return level; }
|
||||
public void setLevel(String level) { this.level = level; }
|
||||
public Integer getPrice() { return price; }
|
||||
public void setPrice(Integer price) { this.price = price; }
|
||||
public List<User> getUsers() { return users; }
|
||||
public String getLevel() {
|
||||
return level;
|
||||
}
|
||||
|
||||
public void setLevel(String level) {
|
||||
this.level = level;
|
||||
}
|
||||
|
||||
public Integer getPrice() {
|
||||
return price;
|
||||
}
|
||||
|
||||
public void setPrice(Integer price) {
|
||||
this.price = price;
|
||||
}
|
||||
|
||||
public List<User> getUsers() {
|
||||
return users;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,46 @@
|
||||
package ru.ulstu.is.server.mapper;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
import ru.ulstu.is.server.dto.SubscriptionRq;
|
||||
|
||||
import ru.ulstu.is.server.dto.SubscriptionRs;
|
||||
import ru.ulstu.is.server.dto.SubscriptionRq;
|
||||
import ru.ulstu.is.server.entity.Subscription;
|
||||
|
||||
@Component
|
||||
public class SubscriptionMapper {
|
||||
|
||||
public Subscription toEntity(SubscriptionRq rq) {
|
||||
Subscription s = new Subscription();
|
||||
s.setLevel(rq.getLevel());
|
||||
s.setPrice(rq.getPrice());
|
||||
s.setDiscount(rq.getDiscount() != null ? rq.getDiscount() : 0);
|
||||
return s;
|
||||
}
|
||||
|
||||
public void updateEntity(Subscription s, SubscriptionRq rq) {
|
||||
s.setLevel(rq.getLevel());
|
||||
s.setPrice(rq.getPrice());
|
||||
s.setDiscount(rq.getDiscount() != null ? rq.getDiscount() : 0);
|
||||
}
|
||||
|
||||
public SubscriptionRs toRs(Subscription s) {
|
||||
return new SubscriptionRs(s.getId(), s.getLevel(), s.getPrice());
|
||||
int price = s.getPrice() != null ? s.getPrice() : 0;
|
||||
int discount = s.getDiscount() != null ? s.getDiscount() : 0;
|
||||
if (discount < 0) {
|
||||
discount = 0;
|
||||
}
|
||||
if (discount > 100) {
|
||||
discount = 100;
|
||||
}
|
||||
|
||||
int priceWithDiscount = Math.round(price * (100 - discount) / 100.0f);
|
||||
|
||||
return new SubscriptionRs(
|
||||
s.getId(),
|
||||
s.getLevel(),
|
||||
price,
|
||||
discount,
|
||||
priceWithDiscount
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,32 @@
|
||||
package ru.ulstu.is.server.mvc;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import java.util.List;
|
||||
|
||||
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 org.springframework.web.bind.annotation.GetMapping;
|
||||
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.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import ru.ulstu.is.server.dto.DirectorRefRq;
|
||||
import ru.ulstu.is.server.dto.DirectorRs;
|
||||
import ru.ulstu.is.server.dto.GenreRefRq;
|
||||
import ru.ulstu.is.server.dto.GenreRs;
|
||||
import ru.ulstu.is.server.dto.MovieRq;
|
||||
import ru.ulstu.is.server.dto.MovieRs;
|
||||
import ru.ulstu.is.server.dto.PageHelper;
|
||||
import ru.ulstu.is.server.dto.PageRs;
|
||||
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 {
|
||||
@@ -25,16 +37,15 @@ public class MovieMvcController {
|
||||
private final MovieSearchSession movieSearchSession;
|
||||
|
||||
public MovieMvcController(MovieService movieService,
|
||||
GenreService genreService,
|
||||
DirectorService directorService,
|
||||
MovieSearchSession movieSearchSession) {
|
||||
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,
|
||||
@@ -42,15 +53,18 @@ public class MovieMvcController {
|
||||
@RequestParam(required = false) String title,
|
||||
@RequestParam(required = false) Long genreId,
|
||||
@RequestParam(required = false) Float minGrade,
|
||||
@RequestParam(required = false) Boolean exact,
|
||||
Model model
|
||||
) {
|
||||
int pageNumber = (page == null || page < 1) ? 1 : page;
|
||||
int pageSize = (size == null || size < 1) ? 12 : size;
|
||||
Boolean exactMatch = exact;
|
||||
|
||||
boolean filtersPassedInRequest =
|
||||
(title != null && !title.isBlank())
|
||||
|| genreId != null
|
||||
|| minGrade != null;
|
||||
boolean filtersPassedInRequest
|
||||
= (title != null && !title.isBlank())
|
||||
|| genreId != null
|
||||
|| minGrade != null
|
||||
|| exact != null;
|
||||
|
||||
if (!filtersPassedInRequest) {
|
||||
if (movieSearchSession.getLastTitle() != null) {
|
||||
@@ -65,22 +79,27 @@ public class MovieMvcController {
|
||||
if (movieSearchSession.getLastPageSize() != null) {
|
||||
pageSize = movieSearchSession.getLastPageSize();
|
||||
}
|
||||
if (movieSearchSession.getLastExactMatch() != null) {
|
||||
exactMatch = movieSearchSession.getLastExactMatch();
|
||||
}
|
||||
} else {
|
||||
movieSearchSession.setLastTitle(title);
|
||||
movieSearchSession.setLastGenreId(genreId);
|
||||
movieSearchSession.setLastMinGrade(minGrade);
|
||||
movieSearchSession.setLastExactMatch(exactMatch);
|
||||
}
|
||||
|
||||
movieSearchSession.setLastPageSize(pageSize);
|
||||
|
||||
boolean hasFilters =
|
||||
(title != null && !title.isBlank())
|
||||
|| genreId != null
|
||||
|| minGrade != null;
|
||||
boolean hasFilters
|
||||
= (title != null && !title.isBlank())
|
||||
|| genreId != null
|
||||
|| minGrade != null
|
||||
|| Boolean.TRUE.equals(exactMatch);
|
||||
|
||||
PageRs<MovieRs> pageRs;
|
||||
if (hasFilters) {
|
||||
pageRs = movieService.search(title, genreId, minGrade, pageNumber, pageSize);
|
||||
pageRs = movieService.search(title, genreId, minGrade, exactMatch, pageNumber, pageSize);
|
||||
} else {
|
||||
Pageable pageable = PageHelper.toPageable(pageNumber, pageSize);
|
||||
pageRs = movieService.getPage(pageable);
|
||||
@@ -95,11 +114,11 @@ public class MovieMvcController {
|
||||
model.addAttribute("titleFilter", title);
|
||||
model.addAttribute("genreIdFilter", genreId);
|
||||
model.addAttribute("minGradeFilter", minGrade);
|
||||
model.addAttribute("exactMatchFilter", Boolean.TRUE.equals(exactMatch));
|
||||
|
||||
return "movies/list";
|
||||
}
|
||||
|
||||
|
||||
@GetMapping("/new")
|
||||
public String showCreateForm(Model model) {
|
||||
MovieRq movieRq = new MovieRq();
|
||||
|
||||
@@ -50,23 +50,34 @@ public class MovieService {
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public PageRs<MovieRs> search(String title, Long genreId, Float minGrade, int page, int size) {
|
||||
public PageRs<MovieRs> search(String title,
|
||||
Long genreId,
|
||||
Float minGrade,
|
||||
Boolean exactMatch,
|
||||
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 (Boolean.TRUE.equals(exactMatch)) {
|
||||
String t = title;
|
||||
stream = stream.filter(m
|
||||
-> m.getTitle() != null && m.getTitle().equalsIgnoreCase(t)
|
||||
);
|
||||
} else {
|
||||
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)
|
||||
stream = stream.filter(m
|
||||
-> m.getGenre() != null
|
||||
&& m.getGenre().getId() != null
|
||||
&& m.getGenre().getId().equals(genreId)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -114,7 +125,6 @@ public class MovieService {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@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"));
|
||||
|
||||
@@ -7,40 +7,49 @@ import org.springframework.web.context.annotation.SessionScope;
|
||||
@SessionScope
|
||||
public class MovieSearchSession {
|
||||
|
||||
private String lastTitle;
|
||||
private Long lastGenreId;
|
||||
private Float lastMinGrade;
|
||||
private Integer lastPageSize;
|
||||
private String lastTitle;
|
||||
private Long lastGenreId;
|
||||
private Float lastMinGrade;
|
||||
private Integer lastPageSize;
|
||||
private Boolean lastExactMatch;
|
||||
|
||||
public String getLastTitle() {
|
||||
return lastTitle;
|
||||
}
|
||||
public String getLastTitle() {
|
||||
return lastTitle;
|
||||
}
|
||||
|
||||
public void setLastTitle(String lastTitle) {
|
||||
this.lastTitle = lastTitle;
|
||||
}
|
||||
public void setLastTitle(String lastTitle) {
|
||||
this.lastTitle = lastTitle;
|
||||
}
|
||||
|
||||
public Long getLastGenreId() {
|
||||
return lastGenreId;
|
||||
}
|
||||
public Long getLastGenreId() {
|
||||
return lastGenreId;
|
||||
}
|
||||
|
||||
public void setLastGenreId(Long lastGenreId) {
|
||||
this.lastGenreId = lastGenreId;
|
||||
}
|
||||
public void setLastGenreId(Long lastGenreId) {
|
||||
this.lastGenreId = lastGenreId;
|
||||
}
|
||||
|
||||
public Float getLastMinGrade() {
|
||||
return lastMinGrade;
|
||||
}
|
||||
public Float getLastMinGrade() {
|
||||
return lastMinGrade;
|
||||
}
|
||||
|
||||
public void setLastMinGrade(Float lastMinGrade) {
|
||||
this.lastMinGrade = lastMinGrade;
|
||||
}
|
||||
public void setLastMinGrade(Float lastMinGrade) {
|
||||
this.lastMinGrade = lastMinGrade;
|
||||
}
|
||||
|
||||
public Integer getLastPageSize() {
|
||||
return lastPageSize;
|
||||
}
|
||||
public Integer getLastPageSize() {
|
||||
return lastPageSize;
|
||||
}
|
||||
|
||||
public void setLastPageSize(Integer lastPageSize) {
|
||||
this.lastPageSize = lastPageSize;
|
||||
}
|
||||
public void setLastPageSize(Integer lastPageSize) {
|
||||
this.lastPageSize = lastPageSize;
|
||||
}
|
||||
|
||||
public Boolean getLastExactMatch() {
|
||||
return lastExactMatch;
|
||||
}
|
||||
|
||||
public void setLastExactMatch(Boolean lastExactMatch) {
|
||||
this.lastExactMatch = lastExactMatch;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
databaseChangeLog:
|
||||
- changeSet:
|
||||
id: 4-add-subscription-discount
|
||||
author: busla
|
||||
changes:
|
||||
- addColumn:
|
||||
tableName: SUBSCRIPTIONS
|
||||
columns:
|
||||
- column:
|
||||
name: DISCOUNT
|
||||
type: INT
|
||||
defaultValueNumeric: 0
|
||||
constraints:
|
||||
nullable: false
|
||||
@@ -4,4 +4,7 @@ databaseChangeLog:
|
||||
relativeToChangelogFile: true
|
||||
- include:
|
||||
file: changeset-3-add-review.yaml
|
||||
relativeToChangelogFile: true
|
||||
- include:
|
||||
file: changeset-4-add-subscription-discount.yaml
|
||||
relativeToChangelogFile: true
|
||||
@@ -1,314 +1,175 @@
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background-color: #181818;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
nav a {
|
||||
.navbar-dark .navbar-nav .nav-link {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.card-title{
|
||||
color: white;
|
||||
}
|
||||
|
||||
.navbar-dark .navbar-nav .nav-link:hover,
|
||||
.navbar-dark .navbar-nav .nav-link:focus {
|
||||
color: #FF0033;
|
||||
}
|
||||
|
||||
.navbar-dark .navbar-nav .nav-link.active {
|
||||
color: #fff;
|
||||
margin: 10px;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
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;
|
||||
/* Стили для карточек фильмов */
|
||||
.movie-card {
|
||||
background-color: #282828;
|
||||
color: #fff;
|
||||
border: 1px solid #404040;
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.user_photo {
|
||||
height: 130px;
|
||||
width: 130px;
|
||||
border-radius: 50px;
|
||||
.movie-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 4px 15px rgba(255, 0, 51, 0.2);
|
||||
}
|
||||
|
||||
.movie-card img {
|
||||
width: 100%;
|
||||
height: 260px;
|
||||
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;
|
||||
/* Стили для таблиц */
|
||||
.table-dark {
|
||||
background-color: #282828;
|
||||
border-radius: 10px;
|
||||
text-align: center;
|
||||
border-color: #404040;
|
||||
}
|
||||
|
||||
.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;
|
||||
.table-dark th,
|
||||
.table-dark td {
|
||||
border-color: #404040;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.account-btn {
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.user-menu:hover .dropdown {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
display: none;
|
||||
position: absolute;
|
||||
/* Стили для форм */
|
||||
.form-control,
|
||||
.form-select {
|
||||
background-color: #333;
|
||||
list-style: none;
|
||||
padding: 5px;
|
||||
left: -22px;
|
||||
margin: 0;
|
||||
border-radius: 5px;
|
||||
width: 120px;
|
||||
border: 1px solid #555;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.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%;
|
||||
}
|
||||
.form-control:focus,
|
||||
.form-select:focus {
|
||||
background-color: #3a3a3a;
|
||||
border-color: #FF0033;
|
||||
color: #fff;
|
||||
box-shadow: 0 0 0 0.25rem rgba(255, 0, 51, 0.25);
|
||||
}
|
||||
|
||||
.form-label {
|
||||
color: #e9ecef;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Стили для кнопок */
|
||||
.btn-primary {
|
||||
background-color: #FF0033;
|
||||
border-color: #FF0033;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: #cc0029;
|
||||
border-color: #cc0029;
|
||||
}
|
||||
|
||||
.btn-outline-light:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
/* Стили для пагинации */
|
||||
.page-link {
|
||||
background-color: #282828;
|
||||
border-color: #404040;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.page-link:hover {
|
||||
background-color: #333;
|
||||
border-color: #555;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.page-item.active .page-link {
|
||||
background-color: #FF0033;
|
||||
border-color: #FF0033;
|
||||
}
|
||||
|
||||
/* Стили для пользовательского фото */
|
||||
.user-photo {
|
||||
height: 130px;
|
||||
width: 130px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
border: 3px solid #FF0033;
|
||||
}
|
||||
|
||||
/* Стили для выпадающего меню пользователя */
|
||||
.dropdown-menu-dark {
|
||||
background-color: #282828;
|
||||
border: 1px solid #404040;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.dropdown-item:hover {
|
||||
background-color: #FF0033;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* Стили для футера */
|
||||
footer {
|
||||
background-color: #202020;
|
||||
border-top: 1px solid #404040;
|
||||
}
|
||||
|
||||
/* Кастомные утилиты */
|
||||
.bg-custom-dark {
|
||||
background-color: #181818 !important;
|
||||
}
|
||||
|
||||
.bg-custom-gray {
|
||||
background-color: #282828 !important;
|
||||
}
|
||||
|
||||
.border-custom {
|
||||
border-color: #404040 !important;
|
||||
}
|
||||
|
||||
/* Адаптивность */
|
||||
@media (max-width: 768px) {
|
||||
header {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
.movie-card img {
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
.user-photo {
|
||||
height: 100px;
|
||||
width: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.side-menu {
|
||||
width: 200px;
|
||||
@media (max-width: 576px) {
|
||||
.container {
|
||||
padding-left: 15px;
|
||||
padding-right: 15px;
|
||||
}
|
||||
|
||||
.movie-list img {
|
||||
width: 120px;
|
||||
|
||||
.movie-card {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
footer {
|
||||
padding: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,24 +4,19 @@
|
||||
<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}">
|
||||
<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>
|
||||
|
||||
@@ -36,7 +31,6 @@
|
||||
</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>
|
||||
@@ -49,7 +43,6 @@
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<!-- Правая часть (пока статично, без useAuth) -->
|
||||
<ul class="navbar-nav ms-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" th:href="@{/login}">Войти</a>
|
||||
@@ -57,14 +50,12 @@
|
||||
<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>
|
||||
@@ -73,7 +64,6 @@
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- Bootstrap JS (для бургер-меню и дропдаунов) -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
</head>
|
||||
<body class="bg-dark text-light">
|
||||
|
||||
<div th:insert="layout :: siteHeader"></div>
|
||||
<div th:replace="layout :: siteHeader"></div>
|
||||
|
||||
|
||||
<main class="container my-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
@@ -57,6 +58,19 @@
|
||||
<input type="hidden" name="page" value="1">
|
||||
</div>
|
||||
|
||||
<div class="col-md-2 d-flex align-items-end">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input"
|
||||
type="checkbox"
|
||||
name="exact"
|
||||
th:checked="${exactMatchFilter}">
|
||||
<label class="form-check-label text-light">
|
||||
Точный поиск
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="col-md-1 d-grid">
|
||||
<button type="submit" class="btn btn-success">OK</button>
|
||||
</div>
|
||||
|
||||
@@ -53,13 +53,28 @@
|
||||
<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"/>
|
||||
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}">
|
||||
th:if="${#fields.hasErrors('price')}"
|
||||
th:errors="*{price}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 🔹 Скидка -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Скидка (%)</label>
|
||||
<input type="number"
|
||||
min="0" max="100"
|
||||
th:field="*{discount}"
|
||||
class="form-control bg-secondary text-light border-0"
|
||||
placeholder="0"/>
|
||||
|
||||
<div class="text-danger mt-1"
|
||||
th:if="${#fields.hasErrors('discount')}"
|
||||
th:errors="*{discount}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -28,10 +28,12 @@
|
||||
<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>
|
||||
<th>ID</th>
|
||||
<th>Уровень</th>
|
||||
<th>Цена, ₽</th>
|
||||
<th>Скидка, %</th>
|
||||
<th>Цена со скидкой, ₽</th>
|
||||
<th class="text-end">Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -39,6 +41,8 @@
|
||||
<td th:text="${sub.id}">1</td>
|
||||
<td th:text="${sub.level}">Базовая</td>
|
||||
<td th:text="${sub.price}">299</td>
|
||||
<td th:text="${sub.discount}">0</td>
|
||||
<td th:text="${sub.priceWithDiscount}">299</td>
|
||||
<td class="text-end">
|
||||
<a th:href="@{|/subscriptions/${sub.id}/edit|}"
|
||||
class="btn btn-sm btn-outline-light me-2">
|
||||
|
||||
Reference in New Issue
Block a user