mirror of
https://github.com/LOBSTERVOVA/Tennis-Site.git
synced 2026-04-17 17:40:49 +03:00
готовый сайт
This commit is contained in:
7
.claude/settings.local.json
Normal file
7
.claude/settings.local.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(xargs ls:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.example.dateplanner.configurations;
|
||||
|
||||
import com.example.dateplanner.models.entities.AppUser;
|
||||
import com.example.dateplanner.models.enums.Role;
|
||||
import com.example.dateplanner.repositories.AppUserRepository;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.CommandLineRunner;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@Slf4j
|
||||
@Configuration
|
||||
public class AppConfig {
|
||||
|
||||
private final String username = "morgan";
|
||||
private final String password = "Admin_123!";
|
||||
|
||||
@Bean
|
||||
public CommandLineRunner prepare(AppUserRepository userRepository, PasswordEncoder encoder) {
|
||||
return args -> {
|
||||
log.info("***************** environments start *******************");
|
||||
for (String key : System.getenv().keySet()) {
|
||||
String[] p = key.split("\\.");
|
||||
if (p.length > 1) {
|
||||
log.info("\"{}\"=\"{}\"", key, System.getenv().get(key));
|
||||
}
|
||||
}
|
||||
log.info("****************** environments end ********************");
|
||||
userRepository.findByPhone(username).flatMap(existingUser -> {
|
||||
log.info("User {} exist.", existingUser.getPhone());
|
||||
return Mono.just(existingUser);
|
||||
}).switchIfEmpty(
|
||||
Mono.defer(() -> {
|
||||
log.info("User not found! Create default admin user: {}, password: {}", username,password);
|
||||
AppUser user = new AppUser();
|
||||
user.setPhone(username);
|
||||
user.setPassword(encoder.encode(password));
|
||||
user.setLastName("Admin");
|
||||
user.setFirstName("Master");
|
||||
user.setRole(Role.ADMIN);
|
||||
return userRepository.save(user);
|
||||
})
|
||||
).flatMap(u -> {
|
||||
log.info("User in db: [{}]", u);
|
||||
return Mono.empty();
|
||||
}).subscribe();
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.example.dateplanner.configurations;
|
||||
|
||||
import io.minio.MinioClient;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
public class MinioConfig {
|
||||
|
||||
@Value("${minio.url}")
|
||||
private String url;
|
||||
|
||||
@Value("${minio.username}")
|
||||
private String username;
|
||||
|
||||
@Value("${minio.password}")
|
||||
private String password;
|
||||
|
||||
@Bean
|
||||
public MinioClient minioClient() {
|
||||
return MinioClient.builder()
|
||||
.endpoint(url)
|
||||
.credentials(username, password)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package com.example.dateplanner.configurations;
|
||||
|
||||
import com.example.dateplanner.configurations.filters.ErrorHandlingFilter;
|
||||
import com.example.dateplanner.configurations.handlers.JwtAuthenticationConverter;
|
||||
import com.example.dateplanner.configurations.handlers.JwtAuthenticationManager;
|
||||
import com.example.dateplanner.configurations.handlers.JwtAuthenticationSuccessHandler;
|
||||
import com.example.dateplanner.configurations.handlers.JwtLogoutSuccessHandler;
|
||||
import com.example.dateplanner.repositories.AppUserRepository;
|
||||
import com.example.dateplanner.services.JwtService;
|
||||
import com.example.dateplanner.services.UserService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity;
|
||||
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
|
||||
import org.springframework.security.config.web.server.SecurityWebFiltersOrder;
|
||||
import org.springframework.security.config.web.server.ServerHttpSecurity;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.web.server.SecurityWebFilterChain;
|
||||
import org.springframework.security.web.server.authentication.AuthenticationWebFilter;
|
||||
import org.springframework.security.web.server.csrf.CookieServerCsrfTokenRepository;
|
||||
import org.springframework.security.web.server.csrf.ServerCsrfTokenRequestAttributeHandler;
|
||||
import org.springframework.security.web.server.savedrequest.ServerRequestCache;
|
||||
import org.springframework.security.web.server.savedrequest.WebSessionServerRequestCache;
|
||||
|
||||
import java.net.URI;
|
||||
|
||||
@Slf4j
|
||||
@Configuration
|
||||
@EnableWebFluxSecurity
|
||||
@RequiredArgsConstructor
|
||||
@EnableReactiveMethodSecurity
|
||||
public class SecurityConfig {
|
||||
private final AppUserRepository userRepository;
|
||||
private final JwtService jwtService;
|
||||
|
||||
@Bean
|
||||
public SecurityWebFilterChain filterChain(ServerHttpSecurity http){
|
||||
ServerCsrfTokenRequestAttributeHandler requestHandler = new ServerCsrfTokenRequestAttributeHandler();
|
||||
requestHandler.setTokenFromMultipartDataEnabled(true);
|
||||
|
||||
AuthenticationWebFilter authenticationWebFilter = new AuthenticationWebFilter(authenticationManager());
|
||||
authenticationWebFilter.setServerAuthenticationConverter(authenticationConverter());
|
||||
|
||||
ErrorHandlingFilter errorHandlingFilter = new ErrorHandlingFilter();
|
||||
|
||||
return http
|
||||
.csrf(csrf -> csrf.csrfTokenRequestHandler(requestHandler).csrfTokenRepository(CookieServerCsrfTokenRepository.withHttpOnlyFalse()))
|
||||
.addFilterAt(authenticationWebFilter, SecurityWebFiltersOrder.AUTHENTICATION)
|
||||
//.addFilterAfter(errorHandlingFilter, SecurityWebFiltersOrder.AUTHENTICATION)
|
||||
.authorizeExchange(exchange -> exchange
|
||||
.pathMatchers("/account/login","/error","/error/**", "/account/logout").permitAll()
|
||||
.pathMatchers("/account/**", "/admin", "/api/admin/**").authenticated()
|
||||
.anyExchange().permitAll()
|
||||
)
|
||||
.formLogin(loginSpec -> loginSpec.loginPage("/account/login").authenticationSuccessHandler(authenticationSuccessHandler()))
|
||||
.logout(logoutSpec -> logoutSpec
|
||||
.logoutUrl("/account/logout") // ← URL для логаута
|
||||
.logoutSuccessHandler(logoutSuccessHandler()))
|
||||
.requestCache(requestCacheSpec -> requestCacheSpec.requestCache(serverRequestCache()))
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ServerRequestCache serverRequestCache() {
|
||||
return new WebSessionServerRequestCache();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public JwtAuthenticationManager authenticationManager() {
|
||||
return new JwtAuthenticationManager(jwtService, userService(), passwordEncoder());
|
||||
}
|
||||
|
||||
@Bean
|
||||
public JwtAuthenticationSuccessHandler authenticationSuccessHandler(){
|
||||
return new JwtAuthenticationSuccessHandler(jwtService,serverRequestCache());
|
||||
}
|
||||
|
||||
@Bean
|
||||
public JwtLogoutSuccessHandler logoutSuccessHandler(){
|
||||
JwtLogoutSuccessHandler logoutSuccessHandler = new JwtLogoutSuccessHandler();
|
||||
logoutSuccessHandler.setLogoutSuccessUrl(URI.create("/"));
|
||||
return logoutSuccessHandler;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public JwtAuthenticationConverter authenticationConverter(){
|
||||
return new JwtAuthenticationConverter();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder(){
|
||||
return new BCryptPasswordEncoder();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public UserService userService(){
|
||||
return new UserService(userRepository,passwordEncoder());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.example.dateplanner.configurations.filters;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.HttpStatusCode;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.WebFilter;
|
||||
import org.springframework.web.server.WebFilterChain;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.net.URI;
|
||||
|
||||
@Slf4j
|
||||
public class ErrorHandlingFilter implements WebFilter {
|
||||
@Override
|
||||
public @NotNull Mono<Void> filter(@NotNull ServerWebExchange exchange, WebFilterChain chain) {
|
||||
//log.info("error handler!");
|
||||
return chain.filter(exchange)
|
||||
.onErrorResume(error -> handleError(exchange, error))
|
||||
.then(Mono.defer(() -> handleNotFound(exchange)));
|
||||
}
|
||||
|
||||
private Mono<Void> handleError(ServerWebExchange exchange, Throwable error) {
|
||||
HttpStatusCode status = determineStatusCode(error);
|
||||
return prepareRedirect(exchange, status);
|
||||
}
|
||||
|
||||
private Mono<Void> handleNotFound(ServerWebExchange exchange) {
|
||||
if (exchange.getResponse().getStatusCode() == null) {
|
||||
return prepareRedirect(exchange, HttpStatus.NOT_FOUND);
|
||||
}
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
private Mono<Void> prepareRedirect(ServerWebExchange exchange, HttpStatusCode status) {
|
||||
exchange.getResponse().setStatusCode(HttpStatus.FOUND); // 302 Redirect
|
||||
String redirectUrl = "/error?status=" + status.value();
|
||||
exchange.getResponse().getHeaders().setLocation(URI.create(redirectUrl));
|
||||
return exchange.getResponse().setComplete();
|
||||
}
|
||||
|
||||
private HttpStatusCode determineStatusCode(Throwable error) {
|
||||
if (error instanceof ResponseStatusException) {
|
||||
return ((ResponseStatusException) error).getStatusCode();
|
||||
}
|
||||
return HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.example.dateplanner.configurations.handlers;
|
||||
|
||||
import com.example.dateplanner.utils.CookieUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpCookie;
|
||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.web.server.authentication.ServerAuthenticationConverter;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@Slf4j
|
||||
public class JwtAuthenticationConverter implements ServerAuthenticationConverter {
|
||||
@Override
|
||||
public Mono<Authentication> convert(ServerWebExchange exchange) {
|
||||
//log.info("start auth converter");
|
||||
String accessToken = extractTokenFromCookie(exchange.getRequest(), CookieUtil.getInstance().getACCESS());
|
||||
String refreshToken = extractTokenFromCookie(exchange.getRequest(), CookieUtil.getInstance().getREFRESH());
|
||||
//log.info("access and refresh tokens: {},{}",accessToken,refreshToken);
|
||||
if(accessToken.equals("") && refreshToken.equals("")){
|
||||
//log.info("token not found");
|
||||
return Mono.empty();
|
||||
}else{
|
||||
//log.info("found access: {}, and refresh: {} tokens", accessToken,refreshToken);
|
||||
return Mono.just(new UsernamePasswordAuthenticationToken(accessToken, refreshToken));
|
||||
}
|
||||
}
|
||||
|
||||
private String extractTokenFromCookie(ServerHttpRequest request, String cookieName) {
|
||||
HttpCookie cookie = request.getCookies().getFirst(cookieName);
|
||||
return cookie != null ? cookie.getValue() : "";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package com.example.dateplanner.configurations.handlers;
|
||||
|
||||
import com.example.dateplanner.services.JwtService;
|
||||
import com.example.dateplanner.services.UserService;
|
||||
import com.example.dateplanner.utils.CookieUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.ResponseCookie;
|
||||
import org.springframework.http.server.reactive.ServerHttpResponse;
|
||||
import org.springframework.security.authentication.BadCredentialsException;
|
||||
import org.springframework.security.authentication.ReactiveAuthenticationManager;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class JwtAuthenticationManager implements ReactiveAuthenticationManager {
|
||||
|
||||
private final JwtService jwt;
|
||||
private final UserService userService;
|
||||
private final PasswordEncoder encoder;
|
||||
|
||||
@Override
|
||||
public Mono<Authentication> authenticate(Authentication authentication) {
|
||||
//log.info("start auth manager");
|
||||
String accessToken = authentication.getPrincipal().toString();
|
||||
String refreshToken = authentication.getCredentials().toString();
|
||||
|
||||
if(jwt.validateToken(accessToken)){
|
||||
//log.info("access token is valid");
|
||||
return Mono.deferContextual(contextView -> {
|
||||
ServerWebExchange exchange = contextView.get(ServerWebExchange.class);
|
||||
String digitalSignature = exchange.getRequest().getHeaders().getFirst("User-Agent");
|
||||
assert digitalSignature != null;
|
||||
if(digitalSignature.equals(jwt.getDigitalSignatureFromToken(accessToken))) {
|
||||
//log.info("the correct digital signature has been presented");
|
||||
return userService.findByUsername(jwt.getUsernameFromToken(accessToken)).flatMap(user -> Mono.just(new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities())));
|
||||
}else{
|
||||
//log.error("the digital signature does not match the signature in the token [{}] | [{}]", digitalSignature, jwt.getDigitalSignatureFromToken(accessToken));
|
||||
ServerHttpResponse response = exchange.getResponse();
|
||||
response.addCookie(ResponseCookie.from(CookieUtil.getInstance().getACCESS(), "")
|
||||
.httpOnly(true)
|
||||
.path("/")
|
||||
.maxAge(0)
|
||||
.build());
|
||||
response.addCookie(ResponseCookie.from(CookieUtil.getInstance().getREFRESH(), "")
|
||||
.httpOnly(true)
|
||||
.path("/")
|
||||
.maxAge(0)
|
||||
.build());
|
||||
response.addCookie(ResponseCookie.from(CookieUtil.getInstance().getSESSION(), "")
|
||||
.httpOnly(true)
|
||||
.path("/")
|
||||
.maxAge(0)
|
||||
.build());
|
||||
return Mono.error(new BadCredentialsException("Authentication failed"));
|
||||
}
|
||||
});
|
||||
}else if(jwt.validateToken(refreshToken)){
|
||||
//log.info("access token is not valid, but refresh token is ok");
|
||||
return Mono.deferContextual(contextView -> {
|
||||
ServerWebExchange exchange = contextView.get(ServerWebExchange.class);
|
||||
String digitalSignature = exchange.getRequest().getHeaders().getFirst("User-Agent");
|
||||
assert digitalSignature != null;
|
||||
if(digitalSignature.equals(jwt.getDigitalSignatureFromToken(refreshToken))) {
|
||||
//log.info("digital signature of refresh token is ok - update jwt");
|
||||
return userService.findByUsername(jwt.getUsernameFromToken(refreshToken)).flatMap(user -> {
|
||||
String username = user.getUsername();
|
||||
String newAccessToken = jwt.generateAccessToken(username, digitalSignature);
|
||||
String newRefreshToken = jwt.generateRefreshToken(username, digitalSignature);
|
||||
ResponseCookie accessCookie = ResponseCookie.from(CookieUtil.getInstance().getACCESS(), newAccessToken)
|
||||
.httpOnly(true)
|
||||
.maxAge(Duration.ofMillis(jwt.getAccessExpiration()))
|
||||
.path("/")
|
||||
.build();
|
||||
ResponseCookie refreshCookie = ResponseCookie.from(CookieUtil.getInstance().getREFRESH(), newRefreshToken)
|
||||
.httpOnly(true)
|
||||
.maxAge(Duration.ofMillis(jwt.getRefreshExpiration()))
|
||||
.path("/")
|
||||
.build();
|
||||
|
||||
exchange.getResponse().addCookie(accessCookie);
|
||||
exchange.getResponse().addCookie(refreshCookie);
|
||||
return Mono.just(new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities()));
|
||||
}).switchIfEmpty(Mono.error(new BadCredentialsException("Authentication failed")));
|
||||
}else{
|
||||
//log.error("the digital signature of refresh token does not match the signature in the token [{}] | [{}]", digitalSignature, jwt.getDigitalSignatureFromToken(refreshToken));
|
||||
ResponseCookie refreshCookie = ResponseCookie.from(CookieUtil.getInstance().getREFRESH(), "")
|
||||
.httpOnly(true)
|
||||
.path("/")
|
||||
.maxAge(0)
|
||||
.build();
|
||||
exchange.getResponse().addCookie(refreshCookie);
|
||||
return baseAuth(authentication);
|
||||
}
|
||||
});
|
||||
}else{
|
||||
return baseAuth(authentication);
|
||||
}
|
||||
}
|
||||
|
||||
private Mono<Authentication> baseAuth(Authentication authentication){
|
||||
//log.info("access and refresh token is not valid - try authenticate by basic login");
|
||||
String username = authentication.getPrincipal().toString();
|
||||
String password = authentication.getCredentials().toString();
|
||||
return userService.findByUsername(username).flatMap(user -> {
|
||||
if(encoder.matches(password,user.getPassword())){
|
||||
return Mono.just(new UsernamePasswordAuthenticationToken(user,null,user.getAuthorities()));
|
||||
} else{
|
||||
return Mono.error(new BadCredentialsException("Authentication failed"));
|
||||
}
|
||||
}).cast(Authentication.class).switchIfEmpty(Mono.error(new BadCredentialsException("Authentication failed")));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.example.dateplanner.configurations.handlers;
|
||||
|
||||
import com.example.dateplanner.services.JwtService;
|
||||
import com.example.dateplanner.utils.CookieUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseCookie;
|
||||
import org.springframework.http.server.reactive.ServerHttpResponse;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.web.server.WebFilterExchange;
|
||||
import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler;
|
||||
import org.springframework.security.web.server.savedrequest.ServerRequestCache;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.net.URI;
|
||||
import java.time.Duration;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class JwtAuthenticationSuccessHandler implements ServerAuthenticationSuccessHandler {
|
||||
|
||||
private final JwtService jwt;
|
||||
private final ServerRequestCache requestCache;
|
||||
|
||||
@Override
|
||||
public Mono<Void> onAuthenticationSuccess(WebFilterExchange webFilterExchange, Authentication authentication) {
|
||||
//log.info("auth login success");
|
||||
String username = authentication.getName();
|
||||
String digitalSignature = webFilterExchange.getExchange().getRequest().getHeaders().getFirst("User-Agent");
|
||||
String accessToken = jwt.generateAccessToken(username, digitalSignature);
|
||||
String refreshToken = jwt.generateRefreshToken(username, digitalSignature);
|
||||
ResponseCookie accessCookie = ResponseCookie.from(CookieUtil.getInstance().getACCESS(), accessToken)
|
||||
.httpOnly(true)
|
||||
.maxAge(Duration.ofMillis(jwt.getAccessExpiration()))
|
||||
.path("/")
|
||||
.build();
|
||||
ResponseCookie refreshCookie = ResponseCookie.from(CookieUtil.getInstance().getREFRESH(), refreshToken)
|
||||
.httpOnly(true)
|
||||
.maxAge(Duration.ofMillis(jwt.getRefreshExpiration()))
|
||||
.path("/")
|
||||
.build();
|
||||
|
||||
ServerHttpResponse response = webFilterExchange.getExchange().getResponse();
|
||||
response.addCookie(accessCookie);
|
||||
response.addCookie(refreshCookie);
|
||||
|
||||
return requestCache.getRedirectUri(webFilterExchange.getExchange())
|
||||
.defaultIfEmpty(URI.create("/"))
|
||||
.flatMap(redirectUri -> {
|
||||
response.setStatusCode(HttpStatus.FOUND);
|
||||
webFilterExchange.getExchange().getResponse().getHeaders().setLocation(redirectUri);
|
||||
return webFilterExchange.getExchange().getResponse().setComplete();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.example.dateplanner.configurations.handlers;
|
||||
|
||||
import com.example.dateplanner.utils.CookieUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.ResponseCookie;
|
||||
import org.springframework.http.server.reactive.ServerHttpResponse;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.web.server.WebFilterExchange;
|
||||
import org.springframework.security.web.server.authentication.logout.RedirectServerLogoutSuccessHandler;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@Slf4j
|
||||
public class JwtLogoutSuccessHandler extends RedirectServerLogoutSuccessHandler {
|
||||
@Override
|
||||
public Mono<Void> onLogoutSuccess(WebFilterExchange webFilterExchange, Authentication authentication) {
|
||||
//log.info("auth logout success");
|
||||
ServerHttpResponse response = webFilterExchange.getExchange().getResponse();
|
||||
response.addCookie(ResponseCookie.from(CookieUtil.getInstance().getACCESS(), "")
|
||||
.httpOnly(true)
|
||||
.path("/")
|
||||
.maxAge(0)
|
||||
.build());
|
||||
response.addCookie(ResponseCookie.from(CookieUtil.getInstance().getREFRESH(), "")
|
||||
.httpOnly(true)
|
||||
.path("/")
|
||||
.maxAge(0)
|
||||
.build());
|
||||
return super.onLogoutSuccess(webFilterExchange, authentication);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.example.dateplanner.controllers.advice;
|
||||
|
||||
import com.example.dateplanner.models.entities.AppUser;
|
||||
import com.example.dateplanner.services.UserService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.PropertySource;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
@ControllerAdvice
|
||||
@RequiredArgsConstructor
|
||||
@PropertySource("classpath:application.properties")
|
||||
public class SecurityAdviceController {
|
||||
|
||||
private final UserService userService;
|
||||
|
||||
@Value("${minio.cdn}")
|
||||
private String cdn;
|
||||
|
||||
@ModelAttribute(name = "cdn")
|
||||
public Mono<String> cdnUrl() {
|
||||
return Mono.just(cdn);
|
||||
}
|
||||
|
||||
@ModelAttribute(name = "auth")
|
||||
public Mono<Boolean> isAuthenticate(@AuthenticationPrincipal AppUser user){
|
||||
if(user != null){
|
||||
return Mono.just(true);
|
||||
}else{
|
||||
return Mono.just(false);
|
||||
}
|
||||
}
|
||||
|
||||
@ModelAttribute(name = "user")
|
||||
public Mono<AppUser> user(@AuthenticationPrincipal AppUser user){
|
||||
if(user != null){
|
||||
return userService.getUserByUuid(user.getUuid());
|
||||
}else{
|
||||
return Mono.empty();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,453 @@
|
||||
package com.example.dateplanner.controllers.rest;
|
||||
|
||||
import com.example.dateplanner.dto.ClubWithCourtsDto;
|
||||
import com.example.dateplanner.dto.CourtDto;
|
||||
import com.example.dateplanner.dto.EventApplicationDto;
|
||||
import com.example.dateplanner.dto.EventDto;
|
||||
import com.example.dateplanner.dto.PageResponse;
|
||||
import com.example.dateplanner.models.entities.AppUser;
|
||||
import com.example.dateplanner.models.entities.Court;
|
||||
import com.example.dateplanner.models.entities.EventApplication;
|
||||
import com.example.dateplanner.models.entities.TennisClub;
|
||||
import com.example.dateplanner.models.entities.TennisEvent;
|
||||
import com.example.dateplanner.repositories.CourtRepository;
|
||||
import com.example.dateplanner.repositories.EventApplicationRepository;
|
||||
import com.example.dateplanner.repositories.TennisClubRepository;
|
||||
import com.example.dateplanner.repositories.TennisEventRepository;
|
||||
import com.example.dateplanner.services.ApplicationService;
|
||||
import com.example.dateplanner.services.MinioService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.codec.multipart.FilePart;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/admin")
|
||||
@RequiredArgsConstructor
|
||||
public class ApiAdminController {
|
||||
|
||||
private final TennisClubRepository tennisClubRepository;
|
||||
private final TennisEventRepository tennisEventRepository;
|
||||
private final CourtRepository courtRepository;
|
||||
private final EventApplicationRepository applicationRepository;
|
||||
private final ApplicationService applicationService;
|
||||
private final MinioService minioService;
|
||||
|
||||
// ── Загрузка изображений ───────────────────────────────────────────────────
|
||||
|
||||
@PostMapping(value = "/upload/image", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
public Mono<Map<String, String>> uploadImage(
|
||||
@RequestPart("file") FilePart filePart,
|
||||
@AuthenticationPrincipal AppUser user) {
|
||||
return minioService.uploadImage(filePart)
|
||||
.map(url -> Map.of("url", url));
|
||||
}
|
||||
|
||||
// ── Клубы ─────────────────────────────────────────────────────────────────
|
||||
|
||||
@GetMapping("/clubs")
|
||||
public Flux<ClubWithCourtsDto> getMyClubs(@AuthenticationPrincipal AppUser user) {
|
||||
return tennisClubRepository.findAll()
|
||||
.filter(c -> c.getOwnerUuid() != null && c.getOwnerUuid().equals(user.getUuid()))
|
||||
.map(this::convertToClubWithCourtsDto);
|
||||
}
|
||||
|
||||
@PostMapping("/clubs")
|
||||
public Mono<TennisClub> createClub(@RequestBody TennisClub club,
|
||||
@AuthenticationPrincipal AppUser user) {
|
||||
club.setOwnerUuid(user.getUuid());
|
||||
club.setCreatedAt(LocalDateTime.now());
|
||||
club.setUpdatedAt(LocalDateTime.now());
|
||||
return tennisClubRepository.save(club);
|
||||
}
|
||||
|
||||
@PutMapping("/clubs/{id}")
|
||||
public Mono<TennisClub> updateClub(@PathVariable UUID id,
|
||||
@RequestBody TennisClub body,
|
||||
@AuthenticationPrincipal AppUser user) {
|
||||
return tennisClubRepository.findById(id)
|
||||
.switchIfEmpty(Mono.error(new ResponseStatusException(HttpStatus.NOT_FOUND)))
|
||||
.flatMap(existing -> {
|
||||
if (!existing.getOwnerUuid().equals(user.getUuid())) {
|
||||
return Mono.error(new ResponseStatusException(HttpStatus.FORBIDDEN));
|
||||
}
|
||||
existing.setName(body.getName());
|
||||
existing.setDescription(body.getDescription());
|
||||
existing.setAddress(body.getAddress());
|
||||
existing.setPhone(body.getPhone());
|
||||
existing.setEmail(body.getEmail());
|
||||
existing.setWebsite(body.getWebsite());
|
||||
existing.setConveniences(body.getConveniences());
|
||||
existing.setImages(body.getImages());
|
||||
existing.setContactPhone(body.getContactPhone());
|
||||
existing.setContactPerson(body.getContactPerson());
|
||||
existing.setTelegramLink(body.getTelegramLink());
|
||||
existing.setUpdatedAt(LocalDateTime.now());
|
||||
return tennisClubRepository.save(existing);
|
||||
});
|
||||
}
|
||||
|
||||
// ── Мероприятия ───────────────────────────────────────────────────────────
|
||||
|
||||
@GetMapping("/events")
|
||||
public Mono<PageResponse<EventDto>> getMyEvents(
|
||||
@AuthenticationPrincipal AppUser user,
|
||||
@RequestParam(defaultValue = "0") int page,
|
||||
@RequestParam(defaultValue = "10") int size) {
|
||||
|
||||
return tennisEventRepository.findAll()
|
||||
.skip((long) page * size)
|
||||
.take(size)
|
||||
.collectList()
|
||||
.zipWith(tennisEventRepository.findAll().count())
|
||||
.flatMap(tuple -> {
|
||||
List<TennisEvent> events = tuple.getT1();
|
||||
long total = tuple.getT2();
|
||||
Set<UUID> clubIds = events.stream()
|
||||
.map(TennisEvent::getClubUuid)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toSet());
|
||||
return tennisClubRepository.findAllById(clubIds)
|
||||
.collectMap(TennisClub::getUuid, TennisClub::getName)
|
||||
.map(clubMap -> {
|
||||
List<EventDto> dtos = events.stream()
|
||||
.map(e -> convertToEventDto(e, clubMap))
|
||||
.collect(Collectors.toList());
|
||||
return new PageResponse<>(dtos, page, size, total);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@GetMapping("/events/by-club/{clubId}")
|
||||
public Flux<Map<String, Object>> getEventsByClub(@PathVariable UUID clubId,
|
||||
@AuthenticationPrincipal AppUser user) {
|
||||
return tennisEventRepository.findByClubUuid(clubId)
|
||||
.sort(Comparator.comparing(TennisEvent::getStartDateTime,
|
||||
Comparator.nullsLast(Comparator.naturalOrder())))
|
||||
.map(e -> {
|
||||
Map<String, Object> m = new HashMap<>();
|
||||
m.put("id", e.getUuid());
|
||||
m.put("title", e.getTitle());
|
||||
m.put("startDateTime", e.getStartDateTime() != null
|
||||
? e.getStartDateTime().toString() : null);
|
||||
return m;
|
||||
});
|
||||
}
|
||||
|
||||
@PatchMapping("/events/{id}/toggle-registration")
|
||||
public Mono<Map<String, Object>> toggleRegistration(@PathVariable UUID id,
|
||||
@AuthenticationPrincipal AppUser user) {
|
||||
return tennisEventRepository.findById(id)
|
||||
.switchIfEmpty(Mono.error(new ResponseStatusException(HttpStatus.NOT_FOUND)))
|
||||
.flatMap(event -> {
|
||||
boolean next = !Boolean.TRUE.equals(event.getRegistrationOpen());
|
||||
event.setRegistrationOpen(next);
|
||||
event.setUpdatedAt(LocalDateTime.now());
|
||||
return tennisEventRepository.save(event);
|
||||
})
|
||||
.map(event -> {
|
||||
Map<String, Object> m = new HashMap<>();
|
||||
m.put("registrationOpen", event.getRegistrationOpen());
|
||||
return m;
|
||||
});
|
||||
}
|
||||
|
||||
@PostMapping("/events")
|
||||
public Mono<TennisEvent> createEvent(@RequestBody TennisEvent event,
|
||||
@AuthenticationPrincipal AppUser user) {
|
||||
event.setCreatedAt(LocalDateTime.now());
|
||||
event.setUpdatedAt(LocalDateTime.now());
|
||||
return tennisEventRepository.save(event);
|
||||
}
|
||||
|
||||
@PutMapping("/events/{id}")
|
||||
public Mono<TennisEvent> updateEvent(@PathVariable UUID id,
|
||||
@RequestBody TennisEvent body,
|
||||
@AuthenticationPrincipal AppUser user) {
|
||||
return tennisEventRepository.findById(id)
|
||||
.switchIfEmpty(Mono.error(new ResponseStatusException(HttpStatus.NOT_FOUND)))
|
||||
.flatMap(existing -> {
|
||||
existing.setTitle(body.getTitle());
|
||||
existing.setDescription(body.getDescription());
|
||||
existing.setType(body.getType());
|
||||
existing.setClubUuid(body.getClubUuid());
|
||||
existing.setStartDateTime(body.getStartDateTime());
|
||||
existing.setEndDateTime(body.getEndDateTime());
|
||||
existing.setMaxParticipants(body.getMaxParticipants());
|
||||
existing.setTakePartPrice(body.getTakePartPrice());
|
||||
existing.setRequirements(body.getRequirements());
|
||||
existing.setContactPhone(body.getContactPhone());
|
||||
existing.setImageUrl(body.getImageUrl());
|
||||
existing.setUpdatedAt(LocalDateTime.now());
|
||||
return tennisEventRepository.save(existing);
|
||||
});
|
||||
}
|
||||
|
||||
// ── Заявки ────────────────────────────────────────────────────────────────
|
||||
|
||||
@GetMapping("/applications")
|
||||
public Mono<PageResponse<EventApplicationDto>> getApplications(
|
||||
@AuthenticationPrincipal AppUser user,
|
||||
@RequestParam(required = false) UUID eventId,
|
||||
@RequestParam(required = false) UUID clubId,
|
||||
@RequestParam(defaultValue = "PENDING") String status,
|
||||
@RequestParam(defaultValue = "0") int page,
|
||||
@RequestParam(defaultValue = "20") int size) {
|
||||
|
||||
boolean allStatuses = "ALL".equalsIgnoreCase(status);
|
||||
|
||||
Flux<EventApplication> source;
|
||||
if (eventId != null) {
|
||||
source = allStatuses
|
||||
? applicationRepository.findByEventUuid(eventId)
|
||||
: applicationRepository.findByEventUuidAndStatus(eventId, status.toUpperCase());
|
||||
} else if (clubId != null) {
|
||||
source = allStatuses
|
||||
? applicationRepository.findByClubUuid(clubId)
|
||||
: applicationRepository.findByClubUuidAndStatus(clubId, status.toUpperCase());
|
||||
} else {
|
||||
source = allStatuses
|
||||
? applicationRepository.findAll()
|
||||
: applicationRepository.findByStatus(status.toUpperCase());
|
||||
}
|
||||
|
||||
return source.collectList()
|
||||
.flatMap(all -> {
|
||||
long total = all.size();
|
||||
List<EventApplication> paged = all.stream()
|
||||
.sorted(Comparator.comparing(EventApplication::getCreatedAt,
|
||||
Comparator.nullsLast(Comparator.reverseOrder())))
|
||||
.skip((long) page * size)
|
||||
.limit(size)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
Set<UUID> eventUuids = paged.stream()
|
||||
.map(EventApplication::getEventUuid)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toSet());
|
||||
Set<UUID> clubUuids = paged.stream()
|
||||
.map(EventApplication::getClubUuid)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
Mono<Map<UUID, String>> eventTitlesMono = tennisEventRepository.findAllById(eventUuids)
|
||||
.collectMap(TennisEvent::getUuid, TennisEvent::getTitle);
|
||||
Mono<Map<UUID, String>> clubNamesMono = tennisClubRepository.findAllById(clubUuids)
|
||||
.collectMap(TennisClub::getUuid, TennisClub::getName);
|
||||
|
||||
return Mono.zip(eventTitlesMono, clubNamesMono)
|
||||
.map(tuple -> {
|
||||
List<EventApplicationDto> dtos = paged.stream()
|
||||
.map(a -> toApplicationDto(a, tuple.getT1(), tuple.getT2()))
|
||||
.collect(Collectors.toList());
|
||||
return new PageResponse<>(dtos, page, size, total);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@GetMapping("/applications/counts")
|
||||
public Mono<Map<String, Long>> getApplicationCounts(
|
||||
@AuthenticationPrincipal AppUser user,
|
||||
@RequestParam(required = false) UUID eventId,
|
||||
@RequestParam(required = false) UUID clubId) {
|
||||
|
||||
Mono<Long> pendingMono, approvedMono, rejectedMono;
|
||||
if (eventId != null) {
|
||||
pendingMono = applicationRepository.countByEventUuidAndStatus(eventId, "PENDING");
|
||||
approvedMono = applicationRepository.countByEventUuidAndStatus(eventId, "APPROVED");
|
||||
rejectedMono = applicationRepository.countByEventUuidAndStatus(eventId, "REJECTED");
|
||||
} else if (clubId != null) {
|
||||
pendingMono = applicationRepository.countByClubUuidAndStatus(clubId, "PENDING");
|
||||
approvedMono = applicationRepository.countByClubUuidAndStatus(clubId, "APPROVED");
|
||||
rejectedMono = applicationRepository.countByClubUuidAndStatus(clubId, "REJECTED");
|
||||
} else {
|
||||
pendingMono = applicationRepository.countByStatus("PENDING");
|
||||
approvedMono = applicationRepository.countByStatus("APPROVED");
|
||||
rejectedMono = applicationRepository.countByStatus("REJECTED");
|
||||
}
|
||||
|
||||
return Mono.zip(pendingMono, approvedMono, rejectedMono)
|
||||
.map(t -> {
|
||||
Map<String, Long> m = new HashMap<>();
|
||||
m.put("PENDING", t.getT1());
|
||||
m.put("APPROVED", t.getT2());
|
||||
m.put("REJECTED", t.getT3());
|
||||
return m;
|
||||
});
|
||||
}
|
||||
|
||||
@PatchMapping("/applications/{id}/approve")
|
||||
public Mono<EventApplicationDto> approveApplication(@PathVariable UUID id,
|
||||
@AuthenticationPrincipal AppUser user) {
|
||||
return applicationService.approve(id)
|
||||
.map(app -> toApplicationDto(app, Map.of(), Map.of()));
|
||||
}
|
||||
|
||||
@PatchMapping("/applications/{id}/reject")
|
||||
public Mono<EventApplicationDto> rejectApplication(@PathVariable UUID id,
|
||||
@RequestBody Map<String, String> body,
|
||||
@AuthenticationPrincipal AppUser user) {
|
||||
String reason = body != null ? body.get("reason") : null;
|
||||
return applicationService.reject(id, reason)
|
||||
.map(app -> toApplicationDto(app, Map.of(), Map.of()));
|
||||
}
|
||||
|
||||
// ── Корты ─────────────────────────────────────────────────────────────────
|
||||
|
||||
@GetMapping("/courts/by-club/{clubId}")
|
||||
public Flux<CourtDto> getCourtsByClub(@PathVariable UUID clubId,
|
||||
@AuthenticationPrincipal AppUser user) {
|
||||
return courtRepository.findByClubUuid(clubId).map(this::toCourtDto);
|
||||
}
|
||||
|
||||
@PostMapping("/courts")
|
||||
public Mono<CourtDto> createCourt(@RequestBody Court court,
|
||||
@AuthenticationPrincipal AppUser user) {
|
||||
if (court.getIsVisible() == null) court.setIsVisible(Boolean.TRUE);
|
||||
if (court.getIsOutdoor() == null) court.setIsOutdoor(Boolean.FALSE);
|
||||
court.setCreatedAt(LocalDateTime.now());
|
||||
court.setUpdatedAt(LocalDateTime.now());
|
||||
return courtRepository.save(court)
|
||||
.flatMap(saved -> tennisClubRepository.findById(saved.getClubUuid())
|
||||
.flatMap(club -> {
|
||||
List<UUID> ids = club.getCourtUuids() != null
|
||||
? new ArrayList<>(club.getCourtUuids()) : new ArrayList<>();
|
||||
if (!ids.contains(saved.getUuid())) ids.add(saved.getUuid());
|
||||
club.setCourtUuids(ids);
|
||||
return tennisClubRepository.save(club).thenReturn(saved);
|
||||
})
|
||||
.defaultIfEmpty(saved))
|
||||
.map(this::toCourtDto);
|
||||
}
|
||||
|
||||
@PutMapping("/courts/{id}")
|
||||
public Mono<CourtDto> updateCourt(@PathVariable UUID id,
|
||||
@RequestBody Court body,
|
||||
@AuthenticationPrincipal AppUser user) {
|
||||
return courtRepository.findById(id)
|
||||
.switchIfEmpty(Mono.error(new ResponseStatusException(HttpStatus.NOT_FOUND)))
|
||||
.flatMap(existing -> {
|
||||
existing.setName(body.getName());
|
||||
existing.setDescription(body.getDescription());
|
||||
existing.setPhoto(body.getPhoto());
|
||||
existing.setPrice(body.getPrice());
|
||||
existing.setCover(body.getCover());
|
||||
existing.setIsOutdoor(body.getIsOutdoor() != null ? body.getIsOutdoor() : Boolean.FALSE);
|
||||
existing.setIsVisible(body.getIsVisible() != null ? body.getIsVisible() : Boolean.TRUE);
|
||||
existing.setUpdatedAt(LocalDateTime.now());
|
||||
return courtRepository.save(existing);
|
||||
})
|
||||
.map(this::toCourtDto);
|
||||
}
|
||||
|
||||
@DeleteMapping("/courts/{id}")
|
||||
public Mono<Void> deleteCourt(@PathVariable UUID id,
|
||||
@AuthenticationPrincipal AppUser user) {
|
||||
return courtRepository.findById(id)
|
||||
.switchIfEmpty(Mono.error(new ResponseStatusException(HttpStatus.NOT_FOUND)))
|
||||
.flatMap(court -> {
|
||||
UUID courtUuid = court.getUuid();
|
||||
UUID clubUuid = court.getClubUuid();
|
||||
Mono<Void> deleteMono = courtRepository.deleteById(courtUuid);
|
||||
if (clubUuid == null) return deleteMono;
|
||||
return deleteMono
|
||||
.then(tennisClubRepository.findById(clubUuid))
|
||||
.flatMap(club -> {
|
||||
List<UUID> ids = club.getCourtUuids() != null
|
||||
? new ArrayList<>(club.getCourtUuids()) : new ArrayList<>();
|
||||
ids.remove(courtUuid);
|
||||
club.setCourtUuids(ids);
|
||||
return tennisClubRepository.save(club).then();
|
||||
})
|
||||
.then();
|
||||
});
|
||||
}
|
||||
|
||||
// ── Конвертация ───────────────────────────────────────────────────────────
|
||||
|
||||
private ClubWithCourtsDto convertToClubWithCourtsDto(TennisClub club) {
|
||||
ClubWithCourtsDto dto = new ClubWithCourtsDto();
|
||||
dto.setId(club.getUuid());
|
||||
dto.setName(club.getName());
|
||||
dto.setDescription(club.getDescription());
|
||||
dto.setAddress(club.getAddress());
|
||||
dto.setPhone(club.getPhone());
|
||||
dto.setEmail(club.getEmail());
|
||||
dto.setWebsite(club.getWebsite());
|
||||
dto.setImages(club.getImages());
|
||||
dto.setCourtUuids(club.getCourtUuids());
|
||||
dto.setConveniences(club.getConveniences());
|
||||
dto.setContactPhone(club.getContactPhone());
|
||||
dto.setContactPerson(club.getContactPerson());
|
||||
dto.setTelegramLink(club.getTelegramLink());
|
||||
dto.setOwnerUuid(club.getOwnerUuid());
|
||||
dto.setCreatedAt(club.getCreatedAt());
|
||||
dto.setUpdatedAt(club.getUpdatedAt());
|
||||
dto.setCourts(List.of());
|
||||
return dto;
|
||||
}
|
||||
|
||||
private EventDto convertToEventDto(TennisEvent event, Map<UUID, String> clubMap) {
|
||||
EventDto dto = new EventDto();
|
||||
dto.setId(event.getUuid());
|
||||
dto.setClubUuid(event.getClubUuid());
|
||||
dto.setClubName(event.getClubUuid() != null ? clubMap.get(event.getClubUuid()) : null);
|
||||
dto.setTitle(event.getTitle());
|
||||
dto.setDescription(event.getDescription());
|
||||
dto.setType(event.getType());
|
||||
dto.setStartDateTime(event.getStartDateTime());
|
||||
dto.setEndDateTime(event.getEndDateTime());
|
||||
dto.setMaxParticipants(event.getMaxParticipants());
|
||||
dto.setCurrentParticipants(event.getCurrentParticipants());
|
||||
dto.setTakePartPrice(event.getTakePartPrice());
|
||||
dto.setRequirements(event.getRequirements());
|
||||
dto.setContactPhone(event.getContactPhone());
|
||||
dto.setImageUrl(event.getImageUrl());
|
||||
dto.setRegistrationOpen(event.getRegistrationOpen() == null || event.getRegistrationOpen());
|
||||
dto.setCreatedAt(event.getCreatedAt());
|
||||
dto.setUpdatedAt(event.getUpdatedAt());
|
||||
return dto;
|
||||
}
|
||||
|
||||
private CourtDto toCourtDto(Court court) {
|
||||
CourtDto dto = new CourtDto();
|
||||
dto.setId(court.getUuid());
|
||||
dto.setName(court.getName());
|
||||
dto.setDescription(court.getDescription());
|
||||
dto.setPhoto(court.getPhoto());
|
||||
dto.setPrice(court.getPrice());
|
||||
dto.setCover(court.getCover());
|
||||
dto.setIsOutdoor(court.getIsOutdoor());
|
||||
dto.setIsVisible(court.getIsVisible());
|
||||
return dto;
|
||||
}
|
||||
|
||||
private EventApplicationDto toApplicationDto(EventApplication app,
|
||||
Map<UUID, String> eventTitles, Map<UUID, String> clubNames) {
|
||||
EventApplicationDto dto = new EventApplicationDto();
|
||||
dto.setId(app.getUuid());
|
||||
dto.setEventUuid(app.getEventUuid());
|
||||
dto.setEventTitle(eventTitles.getOrDefault(app.getEventUuid(), ""));
|
||||
dto.setClubUuid(app.getClubUuid());
|
||||
dto.setClubName(clubNames.getOrDefault(app.getClubUuid(), ""));
|
||||
dto.setFullName(app.getFullName());
|
||||
dto.setEmail(app.getEmail());
|
||||
dto.setBirthDate(app.getBirthDate());
|
||||
dto.setGender(app.getGender());
|
||||
dto.setPhone(app.getPhone());
|
||||
dto.setRatingPoints(app.getRatingPoints());
|
||||
dto.setStatus(app.getStatus());
|
||||
dto.setRejectionReason(app.getRejectionReason());
|
||||
dto.setCreatedAt(app.getCreatedAt());
|
||||
return dto;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,325 @@
|
||||
package com.example.dateplanner.controllers.rest;
|
||||
|
||||
import com.example.dateplanner.dto.*;
|
||||
import com.example.dateplanner.models.entities.CourtBookingRequest;
|
||||
import com.example.dateplanner.models.entities.TennisClub;
|
||||
import com.example.dateplanner.models.entities.TennisEvent;
|
||||
import com.example.dateplanner.repositories.CourtBookingRequestRepository;
|
||||
import com.example.dateplanner.repositories.CourtRepository;
|
||||
import com.example.dateplanner.repositories.TennisClubRepository;
|
||||
import com.example.dateplanner.repositories.TennisEventRepository;
|
||||
import com.example.dateplanner.services.ApplicationService;
|
||||
import com.example.dateplanner.services.EmailService;
|
||||
import com.example.dateplanner.services.RegistrationService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
import java.time.format.DateTimeParseException;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api")
|
||||
@RequiredArgsConstructor
|
||||
public class ApiPublicController {
|
||||
|
||||
private final TennisClubRepository tennisClubRepository;
|
||||
private final TennisEventRepository tennisEventRepository;
|
||||
private final CourtRepository courtRepository;
|
||||
private final CourtBookingRequestRepository bookingRequestRepository;
|
||||
private final RegistrationService registrationService;
|
||||
private final ApplicationService applicationService;
|
||||
private final EmailService emailService;
|
||||
|
||||
// ── Публичные данные ──────────────────────────────────────────────────────
|
||||
|
||||
@GetMapping("/clubs")
|
||||
public Flux<TennisClub> getAllClubs() {
|
||||
return tennisClubRepository.findAll();
|
||||
}
|
||||
|
||||
@GetMapping("/clubs/{id}")
|
||||
public Mono<ClubPublicDto> getClubPublic(@PathVariable UUID id) {
|
||||
return tennisClubRepository.findById(id)
|
||||
.switchIfEmpty(Mono.error(new ResponseStatusException(HttpStatus.NOT_FOUND)))
|
||||
.flatMap(club -> {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
|
||||
Mono<List<ClubPublicDto.CourtPublicDto>> courtsMono = courtRepository
|
||||
.findByClubUuid(club.getUuid())
|
||||
.filter(c -> Boolean.TRUE.equals(c.getIsVisible()))
|
||||
.map(c -> {
|
||||
ClubPublicDto.CourtPublicDto dto = new ClubPublicDto.CourtPublicDto();
|
||||
dto.setId(c.getUuid());
|
||||
dto.setName(c.getName());
|
||||
dto.setDescription(c.getDescription());
|
||||
dto.setPhoto(c.getPhoto());
|
||||
dto.setPrice(c.getPrice());
|
||||
dto.setCover(c.getCover());
|
||||
dto.setIsOutdoor(c.getIsOutdoor());
|
||||
return dto;
|
||||
})
|
||||
.collectList();
|
||||
|
||||
Mono<List<EventDto>> upcomingMono = tennisEventRepository
|
||||
.findByClubUuid(club.getUuid())
|
||||
.filter(e -> e.getStartDateTime() != null && !e.getStartDateTime().isBefore(now))
|
||||
.sort(Comparator.comparing(TennisEvent::getStartDateTime))
|
||||
.take(10)
|
||||
.map(e -> toEventDto(e, club.getName()))
|
||||
.collectList();
|
||||
|
||||
Mono<List<EventDto>> pastMono = tennisEventRepository
|
||||
.findByClubUuid(club.getUuid())
|
||||
.filter(e -> e.getStartDateTime() != null && e.getStartDateTime().isBefore(now))
|
||||
.sort(Comparator.comparing(TennisEvent::getStartDateTime, Comparator.reverseOrder()))
|
||||
.take(6)
|
||||
.map(e -> toEventDto(e, club.getName()))
|
||||
.collectList();
|
||||
|
||||
return Mono.zip(courtsMono, upcomingMono, pastMono)
|
||||
.map(tuple -> toClubPublicDto(club, tuple.getT1(), tuple.getT2(), tuple.getT3()));
|
||||
});
|
||||
}
|
||||
|
||||
@GetMapping("/events")
|
||||
public Mono<PageResponse<EventDto>> getEvents(
|
||||
@RequestParam(defaultValue = "0") int page,
|
||||
@RequestParam(defaultValue = "9") int size) {
|
||||
|
||||
return tennisEventRepository.findAll()
|
||||
.skip((long) page * size)
|
||||
.take(size)
|
||||
.collectList()
|
||||
.zipWith(tennisEventRepository.findAll().count())
|
||||
.flatMap(tuple -> {
|
||||
List<TennisEvent> events = tuple.getT1();
|
||||
long total = tuple.getT2();
|
||||
Set<UUID> clubIds = events.stream()
|
||||
.map(TennisEvent::getClubUuid)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toSet());
|
||||
return tennisClubRepository.findAllById(clubIds)
|
||||
.collectMap(TennisClub::getUuid, TennisClub::getName)
|
||||
.map(clubMap -> {
|
||||
List<EventDto> dtos = events.stream()
|
||||
.map(e -> toDto(e, clubMap))
|
||||
.collect(Collectors.toList());
|
||||
return new PageResponse<>(dtos, page, size, total);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ── Бронирование кортов ───────────────────────────────────────────────────
|
||||
|
||||
@PostMapping("/clubs/{clubId}/booking-request")
|
||||
public Mono<ResponseEntity<Map<String, String>>> submitBookingRequest(
|
||||
@PathVariable UUID clubId,
|
||||
@RequestBody CourtBookingRequestDto dto) {
|
||||
|
||||
// Validate required fields
|
||||
if (dto.getRequesterName() == null || dto.getRequesterName().isBlank()) {
|
||||
return badRequest("Укажите ваше имя");
|
||||
}
|
||||
if (dto.getRequesterEmail() == null || dto.getRequesterEmail().isBlank()) {
|
||||
return badRequest("Укажите email для связи");
|
||||
}
|
||||
if (dto.getBookingDate() == null || dto.getBookingDate().isBlank()) {
|
||||
return badRequest("Укажите дату");
|
||||
}
|
||||
if (dto.getStartTime() == null || dto.getStartTime().isBlank()) {
|
||||
return badRequest("Укажите время начала");
|
||||
}
|
||||
if (dto.getDurationMinutes() <= 0 || dto.getDurationMinutes() % 30 != 0) {
|
||||
return badRequest("Длительность должна быть кратна 30 минутам");
|
||||
}
|
||||
|
||||
// Parse date/time
|
||||
LocalDate bookingDate;
|
||||
LocalTime startTime;
|
||||
try {
|
||||
bookingDate = LocalDate.parse(dto.getBookingDate());
|
||||
startTime = LocalTime.parse(dto.getStartTime());
|
||||
} catch (DateTimeParseException e) {
|
||||
return badRequest("Неверный формат даты или времени");
|
||||
}
|
||||
|
||||
// Validate time is on 30-min boundary
|
||||
if (startTime.getMinute() % 30 != 0 || startTime.getSecond() != 0) {
|
||||
return badRequest("Время начала должно быть кратно 30 минутам (например, 10:00 или 10:30)");
|
||||
}
|
||||
|
||||
LocalTime endTime = startTime.plusMinutes(dto.getDurationMinutes());
|
||||
|
||||
return tennisClubRepository.findById(clubId)
|
||||
.switchIfEmpty(Mono.error(new ResponseStatusException(HttpStatus.NOT_FOUND, "Клуб не найден")))
|
||||
.flatMap(club -> {
|
||||
CourtBookingRequest request = new CourtBookingRequest();
|
||||
request.setClubUuid(clubId);
|
||||
request.setCourtUuid(dto.getCourtId());
|
||||
request.setRequesterName(dto.getRequesterName().trim());
|
||||
request.setRequesterEmail(dto.getRequesterEmail().trim());
|
||||
request.setRequesterPhone(dto.getRequesterPhone() != null ? dto.getRequesterPhone().trim() : null);
|
||||
request.setBookingDate(bookingDate);
|
||||
request.setStartTime(startTime);
|
||||
request.setEndTime(endTime);
|
||||
request.setMessage(dto.getMessage());
|
||||
request.setStatus("NEW");
|
||||
request.setCreatedAt(LocalDateTime.now());
|
||||
|
||||
return bookingRequestRepository.save(request).flatMap(saved -> {
|
||||
// Send email to club if email is set
|
||||
String clubEmail = club.getEmail();
|
||||
if (clubEmail != null && !clubEmail.isBlank()) {
|
||||
String courtLine = dto.getCourtId() != null
|
||||
? "<br>Корт UUID: " + dto.getCourtId()
|
||||
: "";
|
||||
String messageLine = dto.getMessage() != null && !dto.getMessage().isBlank()
|
||||
? "<br>Сообщение: " + escapeHtml(dto.getMessage())
|
||||
: "";
|
||||
String phoneLine = request.getRequesterPhone() != null
|
||||
? "<br>Телефон: " + escapeHtml(request.getRequesterPhone())
|
||||
: "";
|
||||
String body = "<h3>Новая заявка на аренду корта</h3>"
|
||||
+ "<b>Клуб:</b> " + escapeHtml(club.getName())
|
||||
+ "<br><b>Имя:</b> " + escapeHtml(request.getRequesterName())
|
||||
+ "<br><b>Email:</b> " + escapeHtml(request.getRequesterEmail())
|
||||
+ phoneLine
|
||||
+ "<br><b>Дата:</b> " + bookingDate
|
||||
+ "<br><b>Время:</b> " + startTime + " — " + endTime
|
||||
+ courtLine
|
||||
+ messageLine;
|
||||
return emailService.send(clubEmail, "Заявка на аренду корта — " + club.getName(), body)
|
||||
.onErrorResume(e -> Mono.empty())
|
||||
.thenReturn(ResponseEntity.ok(Map.of("message", "Заявка отправлена. Клуб свяжется с вами.")));
|
||||
}
|
||||
return Mono.just(ResponseEntity.<Map<String, String>>ok(
|
||||
Map.of("message", "Заявка отправлена. Клуб свяжется с вами.")));
|
||||
});
|
||||
})
|
||||
.onErrorResume(ResponseStatusException.class, e ->
|
||||
Mono.just(ResponseEntity.status(e.getStatusCode())
|
||||
.body(Map.of("message", e.getReason() != null ? e.getReason() : "Ошибка"))))
|
||||
.onErrorResume(e ->
|
||||
Mono.just(ResponseEntity.internalServerError()
|
||||
.body(Map.of("message", "Ошибка при отправке заявки"))));
|
||||
}
|
||||
|
||||
// ── Заявки на участие ─────────────────────────────────────────────────────
|
||||
|
||||
@PostMapping("/events/{eventId}/apply")
|
||||
public Mono<ResponseEntity<Map<String, String>>> applyToEvent(
|
||||
@PathVariable UUID eventId,
|
||||
@RequestBody SubmitApplicationDto dto) {
|
||||
return applicationService.submit(eventId, dto)
|
||||
.map(app -> ResponseEntity.ok(Map.of("message", "Заявка успешно принята")))
|
||||
.onErrorResume(ResponseStatusException.class, e ->
|
||||
Mono.just(ResponseEntity.status(e.getStatusCode())
|
||||
.body(Map.of("message",
|
||||
e.getReason() != null ? e.getReason() : "Ошибка при подаче заявки"))))
|
||||
.onErrorResume(e ->
|
||||
Mono.just(ResponseEntity.internalServerError()
|
||||
.body(Map.of("message", "Ошибка при подаче заявки"))));
|
||||
}
|
||||
|
||||
// ── Регистрация ───────────────────────────────────────────────────────────
|
||||
|
||||
@PostMapping("/auth/register")
|
||||
public Mono<ResponseEntity<Map<String, String>>> register(@RequestBody Map<String, String> body) {
|
||||
String email = body.get("email");
|
||||
if (email == null || email.isBlank()) {
|
||||
return Mono.just(ResponseEntity.badRequest()
|
||||
.body(Map.of("message", "Email обязателен")));
|
||||
}
|
||||
|
||||
return registrationService.register(email.trim().toLowerCase())
|
||||
.then(Mono.just(ResponseEntity.ok(
|
||||
Map.of("message", "Пароль отправлен на " + email))))
|
||||
.onErrorResume(IllegalArgumentException.class, e ->
|
||||
Mono.just(ResponseEntity.badRequest()
|
||||
.body(Map.of("message", e.getMessage()))))
|
||||
.onErrorResume(e ->
|
||||
Mono.just(ResponseEntity.internalServerError()
|
||||
.body(Map.of("message", e.getMessage()))));
|
||||
}
|
||||
|
||||
// ── Конвертация ───────────────────────────────────────────────────────────
|
||||
|
||||
private ClubPublicDto toClubPublicDto(TennisClub club,
|
||||
List<ClubPublicDto.CourtPublicDto> courts,
|
||||
List<EventDto> upcoming,
|
||||
List<EventDto> past) {
|
||||
ClubPublicDto dto = new ClubPublicDto();
|
||||
dto.setId(club.getUuid());
|
||||
dto.setName(club.getName());
|
||||
dto.setDescription(club.getDescription());
|
||||
dto.setAddress(club.getAddress());
|
||||
dto.setPhone(club.getPhone());
|
||||
dto.setEmail(club.getEmail());
|
||||
dto.setWebsite(club.getWebsite());
|
||||
dto.setImages(club.getImages());
|
||||
dto.setConveniences(club.getConveniences());
|
||||
dto.setContactPhone(club.getContactPhone());
|
||||
dto.setContactPerson(club.getContactPerson());
|
||||
dto.setTelegramLink(club.getTelegramLink());
|
||||
dto.setCourts(courts);
|
||||
dto.setUpcomingEvents(upcoming);
|
||||
dto.setPastEvents(past);
|
||||
return dto;
|
||||
}
|
||||
|
||||
private EventDto toEventDto(TennisEvent event, String clubName) {
|
||||
EventDto dto = new EventDto();
|
||||
dto.setId(event.getUuid());
|
||||
dto.setClubUuid(event.getClubUuid());
|
||||
dto.setClubName(clubName);
|
||||
dto.setTitle(event.getTitle());
|
||||
dto.setDescription(event.getDescription());
|
||||
dto.setType(event.getType());
|
||||
dto.setStartDateTime(event.getStartDateTime());
|
||||
dto.setEndDateTime(event.getEndDateTime());
|
||||
dto.setMaxParticipants(event.getMaxParticipants());
|
||||
dto.setCurrentParticipants(event.getCurrentParticipants());
|
||||
dto.setTakePartPrice(event.getTakePartPrice());
|
||||
dto.setContactPhone(event.getContactPhone());
|
||||
dto.setImageUrl(event.getImageUrl());
|
||||
dto.setRegistrationOpen(event.getRegistrationOpen() == null || event.getRegistrationOpen());
|
||||
return dto;
|
||||
}
|
||||
|
||||
private EventDto toDto(TennisEvent event, Map<UUID, String> clubMap) {
|
||||
EventDto dto = new EventDto();
|
||||
dto.setId(event.getUuid());
|
||||
dto.setClubUuid(event.getClubUuid());
|
||||
dto.setClubName(event.getClubUuid() != null ? clubMap.get(event.getClubUuid()) : null);
|
||||
dto.setTitle(event.getTitle());
|
||||
dto.setDescription(event.getDescription());
|
||||
dto.setType(event.getType());
|
||||
dto.setStartDateTime(event.getStartDateTime());
|
||||
dto.setEndDateTime(event.getEndDateTime());
|
||||
dto.setMaxParticipants(event.getMaxParticipants());
|
||||
dto.setCurrentParticipants(event.getCurrentParticipants());
|
||||
dto.setTakePartPrice(event.getTakePartPrice());
|
||||
dto.setContactPhone(event.getContactPhone());
|
||||
dto.setImageUrl(event.getImageUrl());
|
||||
dto.setRegistrationOpen(event.getRegistrationOpen() == null || event.getRegistrationOpen());
|
||||
return dto;
|
||||
}
|
||||
|
||||
private Mono<ResponseEntity<Map<String, String>>> badRequest(String message) {
|
||||
return Mono.just(ResponseEntity.badRequest().body(Map.of("message", message)));
|
||||
}
|
||||
|
||||
private String escapeHtml(String s) {
|
||||
if (s == null) return "";
|
||||
return s.replace("&", "&").replace("<", "<").replace(">", ">");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.example.dateplanner.controllers.web;
|
||||
|
||||
import com.example.dateplanner.models.entities.AppUser;
|
||||
import com.example.dateplanner.services.UserService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.reactive.result.view.Rendering;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
@Slf4j
|
||||
@Controller
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("/account")
|
||||
public class AccountController extends BaseWebController {
|
||||
|
||||
private final UserService userService;
|
||||
|
||||
@GetMapping("/login")
|
||||
public Mono<Rendering> loginPage() {
|
||||
Map<String, Object> model = new HashMap<>();
|
||||
model.put("title", "Login");
|
||||
model.put("index", "login");
|
||||
return addAuthToModel(model).map(m ->
|
||||
Rendering.view("template").model(m).build()
|
||||
);
|
||||
}
|
||||
|
||||
@GetMapping("/profile")
|
||||
public Mono<Rendering> profilePage(@AuthenticationPrincipal AppUser user) {
|
||||
Map<String, Object> model = new HashMap<>();
|
||||
model.put("title", "Profile");
|
||||
model.put("index", "admin-panel");
|
||||
return addAuthToModel(model).map(m ->
|
||||
Rendering.view("template").model(m).build()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.example.dateplanner.controllers.web;
|
||||
|
||||
import com.example.dateplanner.models.entities.AppUser;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public abstract class BaseWebController {
|
||||
|
||||
@Value("${minio.cdn}")
|
||||
private String cdnUrl;
|
||||
|
||||
@Value("${turnstile.site-key:}")
|
||||
private String turnstileSiteKey;
|
||||
|
||||
protected Mono<Map<String, Object>> addAuthToModel(Map<String, Object> model) {
|
||||
return ReactiveSecurityContextHolder.getContext()
|
||||
.map(ctx -> {
|
||||
var authentication = ctx.getAuthentication();
|
||||
if (authentication != null && authentication.isAuthenticated()
|
||||
&& authentication.getPrincipal() instanceof AppUser user) {
|
||||
model.put("auth", true);
|
||||
Map<String, Object> userMap = new HashMap<>();
|
||||
userMap.put("uuid", user.getUuid() != null ? user.getUuid().toString() : null);
|
||||
userMap.put("email", user.getPhone());
|
||||
userMap.put("firstName", user.getFirstName());
|
||||
userMap.put("lastName", user.getLastName());
|
||||
userMap.put("role", user.getRole() != null ? user.getRole().toString() : null);
|
||||
model.put("user", userMap);
|
||||
} else {
|
||||
model.put("auth", false);
|
||||
model.put("user", null);
|
||||
}
|
||||
model.put("cdn", cdnUrl != null ? cdnUrl : "");
|
||||
model.put("turnstileSiteKey", turnstileSiteKey != null ? turnstileSiteKey : "");
|
||||
return model;
|
||||
})
|
||||
.switchIfEmpty(Mono.fromCallable(() -> {
|
||||
model.put("auth", false);
|
||||
model.put("user", null);
|
||||
model.put("cdn", cdnUrl != null ? cdnUrl : "");
|
||||
model.put("turnstileSiteKey", turnstileSiteKey != null ? turnstileSiteKey : "");
|
||||
return model;
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -4,25 +4,47 @@ import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.reactive.result.view.Rendering;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
@Slf4j
|
||||
@Controller
|
||||
@RequiredArgsConstructor
|
||||
public class HomeController {
|
||||
public class HomeController extends BaseWebController {
|
||||
|
||||
@GetMapping("/")
|
||||
public Mono<Rendering> home() {
|
||||
Map<String, Object> model = new HashMap<>();
|
||||
model.put("title", "Home");
|
||||
model.put("index", "home");
|
||||
return Mono.just(
|
||||
Rendering.view("all-html")
|
||||
.model(model)
|
||||
.build()
|
||||
return addAuthToModel(model).map(m ->
|
||||
Rendering.view("template").model(m).build()
|
||||
);
|
||||
}
|
||||
|
||||
@GetMapping("/clubs/{id}")
|
||||
public Mono<Rendering> clubPage(@PathVariable UUID id) {
|
||||
Map<String, Object> model = new HashMap<>();
|
||||
model.put("title", "Клуб");
|
||||
model.put("index", "club");
|
||||
model.put("clubId", id.toString());
|
||||
return addAuthToModel(model).map(m ->
|
||||
Rendering.view("template").model(m).build()
|
||||
);
|
||||
}
|
||||
|
||||
@GetMapping("/admin")
|
||||
public Mono<Rendering> adminPanel() {
|
||||
Map<String, Object> model = new HashMap<>();
|
||||
model.put("title", "Панель управления");
|
||||
model.put("index", "admin-panel");
|
||||
return addAuthToModel(model).map(m ->
|
||||
Rendering.view("template").model(m).build()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
39
src/main/java/com/example/dateplanner/dto/ClubPublicDto.java
Normal file
39
src/main/java/com/example/dateplanner/dto/ClubPublicDto.java
Normal file
@@ -0,0 +1,39 @@
|
||||
package com.example.dateplanner.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@Data
|
||||
public class ClubPublicDto {
|
||||
private UUID id;
|
||||
private String name;
|
||||
private String description;
|
||||
private String address;
|
||||
private String phone;
|
||||
private String email;
|
||||
private String website;
|
||||
private List<String> images;
|
||||
private String conveniences;
|
||||
|
||||
// Public contact fields set by admin
|
||||
private String contactPhone;
|
||||
private String contactPerson;
|
||||
private String telegramLink;
|
||||
|
||||
private List<CourtPublicDto> courts;
|
||||
private List<EventDto> upcomingEvents;
|
||||
private List<EventDto> pastEvents;
|
||||
|
||||
@Data
|
||||
public static class CourtPublicDto {
|
||||
private UUID id;
|
||||
private String name;
|
||||
private String description;
|
||||
private String photo;
|
||||
private Integer price;
|
||||
private String cover;
|
||||
private Boolean isOutdoor;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.example.dateplanner.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@Data
|
||||
public class ClubWithCourtsDto {
|
||||
private UUID id;
|
||||
private String name;
|
||||
private String description;
|
||||
private String address;
|
||||
private String phone;
|
||||
private String email;
|
||||
private String website;
|
||||
private List<String> images;
|
||||
private List<UUID> courtUuids;
|
||||
private String conveniences;
|
||||
private String contactPhone;
|
||||
private String contactPerson;
|
||||
private String telegramLink;
|
||||
private UUID ownerUuid;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
private List<CourtDto> courts;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.example.dateplanner.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
@Data
|
||||
public class CourtBookingRequestDto {
|
||||
private String requesterName;
|
||||
private String requesterEmail;
|
||||
private String requesterPhone;
|
||||
private UUID courtId;
|
||||
private String bookingDate; // ISO date: yyyy-MM-dd
|
||||
private String startTime; // HH:mm (must be on 30-min boundary)
|
||||
private int durationMinutes; // multiple of 30, min 30
|
||||
private String message;
|
||||
}
|
||||
16
src/main/java/com/example/dateplanner/dto/CourtDto.java
Normal file
16
src/main/java/com/example/dateplanner/dto/CourtDto.java
Normal file
@@ -0,0 +1,16 @@
|
||||
package com.example.dateplanner.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import java.util.UUID;
|
||||
|
||||
@Data
|
||||
public class CourtDto {
|
||||
private UUID id;
|
||||
private String name;
|
||||
private String description;
|
||||
private String photo;
|
||||
private Integer price;
|
||||
private String cover;
|
||||
private Boolean isOutdoor;
|
||||
private Boolean isVisible;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.example.dateplanner.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
@Data
|
||||
public class EventApplicationDto {
|
||||
private UUID id;
|
||||
private UUID eventUuid;
|
||||
private String eventTitle;
|
||||
private UUID clubUuid;
|
||||
private String clubName;
|
||||
private String fullName;
|
||||
private String email;
|
||||
private LocalDate birthDate;
|
||||
private String gender;
|
||||
private String phone;
|
||||
private Integer ratingPoints;
|
||||
private String status;
|
||||
private String rejectionReason;
|
||||
private LocalDateTime createdAt;
|
||||
}
|
||||
29
src/main/java/com/example/dateplanner/dto/EventDto.java
Normal file
29
src/main/java/com/example/dateplanner/dto/EventDto.java
Normal file
@@ -0,0 +1,29 @@
|
||||
package com.example.dateplanner.dto;
|
||||
|
||||
import com.example.dateplanner.models.enums.EventType;
|
||||
import lombok.Data;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
@Data
|
||||
public class EventDto {
|
||||
private UUID id;
|
||||
private UUID clubUuid;
|
||||
private String clubName;
|
||||
private String title;
|
||||
private String description;
|
||||
private EventType type;
|
||||
private LocalDateTime startDateTime;
|
||||
private LocalDateTime endDateTime;
|
||||
private Integer maxParticipants;
|
||||
private Integer currentParticipants;
|
||||
private Double takePartPrice;
|
||||
private String rewards;
|
||||
private String requirements;
|
||||
private String contactEmail;
|
||||
private String contactPhone;
|
||||
private String imageUrl;
|
||||
private Boolean registrationOpen = Boolean.TRUE;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
25
src/main/java/com/example/dateplanner/dto/PageResponse.java
Normal file
25
src/main/java/com/example/dateplanner/dto/PageResponse.java
Normal file
@@ -0,0 +1,25 @@
|
||||
package com.example.dateplanner.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class PageResponse<T> {
|
||||
private List<T> content;
|
||||
private int page;
|
||||
private int size;
|
||||
private long totalElements;
|
||||
private int totalPages;
|
||||
private boolean first;
|
||||
private boolean last;
|
||||
|
||||
public PageResponse(List<T> content, int page, int size, long totalElements) {
|
||||
this.content = content;
|
||||
this.page = page;
|
||||
this.size = size;
|
||||
this.totalElements = totalElements;
|
||||
this.totalPages = (int) Math.ceil((double) totalElements / size);
|
||||
this.first = page == 0;
|
||||
this.last = page >= totalPages - 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.example.dateplanner.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class SubmitApplicationDto {
|
||||
private String fullName;
|
||||
private String email;
|
||||
private String birthDate;
|
||||
private String gender;
|
||||
private String phone;
|
||||
private Integer ratingPoints;
|
||||
private String cfTurnstileResponse;
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package com.example.dateplanner.models.entities;
|
||||
|
||||
import com.example.dateplanner.models.enums.Role;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.NonNull;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.relational.core.mapping.Column;
|
||||
import org.springframework.data.relational.core.mapping.Table;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@Table(name = "app_users")
|
||||
public class AppUser implements UserDetails {
|
||||
@Id
|
||||
private UUID uuid;
|
||||
@NonNull
|
||||
private String phone;
|
||||
private String password;
|
||||
@NonNull
|
||||
private String lastName;
|
||||
@NonNull
|
||||
private String firstName;
|
||||
private Role role = Role.USER;
|
||||
|
||||
private boolean enabled = true;
|
||||
@Column("created_at")
|
||||
private LocalDateTime createdAt;
|
||||
@Column("updated_at")
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
@Override
|
||||
public Collection<? extends GrantedAuthority> getAuthorities() {
|
||||
return List.of(new SimpleGrantedAuthority("ROLE_" + getRole().toString()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUsername() {
|
||||
return getPhone();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAccountNonExpired() {
|
||||
return UserDetails.super.isAccountNonExpired();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAccountNonLocked() {
|
||||
return UserDetails.super.isAccountNonLocked();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCredentialsNonExpired() {
|
||||
return UserDetails.super.isCredentialsNonExpired();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnabled() {
|
||||
return UserDetails.super.isEnabled();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.example.dateplanner.models.entities;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.relational.core.mapping.Column;
|
||||
import org.springframework.data.relational.core.mapping.Table;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@Table(name = "courts")
|
||||
public class Court {
|
||||
@Id
|
||||
@Column("uuid")
|
||||
private UUID uuid;
|
||||
|
||||
@Column("club_uuid")
|
||||
private UUID clubUuid;
|
||||
|
||||
private String name;
|
||||
private String description;
|
||||
private String photo;
|
||||
private Integer price;
|
||||
private String cover;
|
||||
|
||||
@Column("is_outdoor")
|
||||
private Boolean isOutdoor = Boolean.FALSE;
|
||||
|
||||
@Column("is_visible")
|
||||
private Boolean isVisible = Boolean.TRUE;
|
||||
|
||||
@Column("created_at")
|
||||
private LocalDateTime createdAt;
|
||||
@Column("updated_at")
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.example.dateplanner.models.entities;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.relational.core.mapping.Column;
|
||||
import org.springframework.data.relational.core.mapping.Table;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
import java.util.UUID;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@Table(name = "court_booking_requests")
|
||||
public class CourtBookingRequest {
|
||||
@Id
|
||||
@Column("uuid")
|
||||
private UUID uuid;
|
||||
|
||||
@Column("club_uuid")
|
||||
private UUID clubUuid;
|
||||
|
||||
@Column("court_uuid")
|
||||
private UUID courtUuid;
|
||||
|
||||
@Column("court_name")
|
||||
private String courtName;
|
||||
|
||||
@Column("requester_name")
|
||||
private String requesterName;
|
||||
|
||||
@Column("requester_email")
|
||||
private String requesterEmail;
|
||||
|
||||
@Column("requester_phone")
|
||||
private String requesterPhone;
|
||||
|
||||
@Column("booking_date")
|
||||
private LocalDate bookingDate;
|
||||
|
||||
@Column("start_time")
|
||||
private LocalTime startTime;
|
||||
|
||||
@Column("end_time")
|
||||
private LocalTime endTime;
|
||||
|
||||
private String message;
|
||||
private String status = "NEW";
|
||||
|
||||
@Column("created_at")
|
||||
private LocalDateTime createdAt;
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.example.dateplanner.models.entities;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.relational.core.mapping.Column;
|
||||
import org.springframework.data.relational.core.mapping.Table;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@Table(name = "event_applications")
|
||||
public class EventApplication {
|
||||
@Id
|
||||
private UUID uuid;
|
||||
|
||||
@Column("event_uuid")
|
||||
private UUID eventUuid;
|
||||
|
||||
@Column("club_uuid")
|
||||
private UUID clubUuid;
|
||||
|
||||
@Column("full_name")
|
||||
private String fullName;
|
||||
|
||||
private String email;
|
||||
|
||||
@Column("birth_date")
|
||||
private LocalDate birthDate;
|
||||
|
||||
private String gender;
|
||||
private String phone;
|
||||
|
||||
@Column("rating_points")
|
||||
private Integer ratingPoints;
|
||||
|
||||
private String status = "PENDING";
|
||||
|
||||
@Column("rejection_reason")
|
||||
private String rejectionReason;
|
||||
|
||||
@Column("created_at")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Column("updated_at")
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.example.dateplanner.models.entities;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.NonNull;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.relational.core.mapping.Column;
|
||||
import org.springframework.data.relational.core.mapping.Table;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@Table(name = "tennis_clubs")
|
||||
public class TennisClub {
|
||||
@Id
|
||||
@Column("uuid")
|
||||
private UUID uuid;
|
||||
@NonNull
|
||||
private String name;
|
||||
private String description;
|
||||
private String address;
|
||||
private String phone;
|
||||
private String email;
|
||||
private String website;
|
||||
private List<String> images;
|
||||
|
||||
@Column("court_uuids")
|
||||
private List<UUID> courtUuids;
|
||||
|
||||
private String conveniences;
|
||||
|
||||
@Column("contact_phone")
|
||||
private String contactPhone;
|
||||
|
||||
@Column("contact_person")
|
||||
private String contactPerson;
|
||||
|
||||
@Column("telegram_link")
|
||||
private String telegramLink;
|
||||
|
||||
@Column("owner_uuid")
|
||||
private UUID ownerUuid;
|
||||
|
||||
@Column("created_at")
|
||||
private LocalDateTime createdAt;
|
||||
@Column("updated_at")
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.example.dateplanner.models.entities;
|
||||
|
||||
import com.example.dateplanner.models.enums.EventType;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.NonNull;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.relational.core.mapping.Column;
|
||||
import org.springframework.data.relational.core.mapping.Table;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@Table(name = "tennis_events")
|
||||
public class TennisEvent {
|
||||
@Id
|
||||
@Column("uuid")
|
||||
private UUID uuid;
|
||||
@NonNull
|
||||
@Column("club_uuid")
|
||||
private UUID clubUuid;
|
||||
@NonNull
|
||||
private String title;
|
||||
private String description;
|
||||
@NonNull
|
||||
private EventType type;
|
||||
|
||||
@Column("start_datetime")
|
||||
@NonNull
|
||||
private LocalDateTime startDateTime;
|
||||
@Column("end_datetime")
|
||||
private LocalDateTime endDateTime;
|
||||
@Column("max_participants")
|
||||
private Integer maxParticipants;
|
||||
@Column("current_participants")
|
||||
private Integer currentParticipants = 0;
|
||||
@Column("take_part_price")
|
||||
private Double takePartPrice;
|
||||
private String rewards;
|
||||
private String requirements;
|
||||
private String contactEmail;
|
||||
private String contactPhone;
|
||||
private String imageUrl;
|
||||
@Column("registration_open")
|
||||
private Boolean registrationOpen = Boolean.TRUE;
|
||||
|
||||
@Column("created_at")
|
||||
private LocalDateTime createdAt;
|
||||
@Column("updated_at")
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.example.dateplanner.models.enums;
|
||||
|
||||
public enum EventType {
|
||||
TOURNAMENT("Турнир"),
|
||||
TRAINING("Тренировка"),
|
||||
LESSON("Урок"),
|
||||
EXHIBITION("Выставочный матч"),
|
||||
CAMP("Лагерь");
|
||||
|
||||
private final String displayName;
|
||||
|
||||
EventType(String displayName) {
|
||||
this.displayName = displayName;
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
}
|
||||
17
src/main/java/com/example/dateplanner/models/enums/Role.java
Normal file
17
src/main/java/com/example/dateplanner/models/enums/Role.java
Normal file
@@ -0,0 +1,17 @@
|
||||
package com.example.dateplanner.models.enums;
|
||||
|
||||
public enum Role {
|
||||
ADMIN("Администратор"),
|
||||
USER ("Пользователь");
|
||||
|
||||
private final String title;
|
||||
|
||||
Role(String title){
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.example.dateplanner.repositories;
|
||||
|
||||
import com.example.dateplanner.models.entities.AppUser;
|
||||
import org.springframework.data.r2dbc.repository.R2dbcRepository;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public interface AppUserRepository extends R2dbcRepository<AppUser, UUID> {
|
||||
|
||||
Mono<AppUser> findByPhone(String phone);
|
||||
Mono<AppUser> findByUsername(String username);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.example.dateplanner.repositories;
|
||||
|
||||
import com.example.dateplanner.models.entities.CourtBookingRequest;
|
||||
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
@Repository
|
||||
public interface CourtBookingRequestRepository extends ReactiveCrudRepository<CourtBookingRequest, UUID> {
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.example.dateplanner.repositories;
|
||||
|
||||
import com.example.dateplanner.models.entities.Court;
|
||||
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
@Repository
|
||||
public interface CourtRepository extends ReactiveCrudRepository<Court, UUID> {
|
||||
Flux<Court> findByClubUuid(UUID clubUuid);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.example.dateplanner.repositories;
|
||||
|
||||
import com.example.dateplanner.models.entities.EventApplication;
|
||||
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public interface EventApplicationRepository extends ReactiveCrudRepository<EventApplication, UUID> {
|
||||
Flux<EventApplication> findByEventUuid(UUID eventUuid);
|
||||
Flux<EventApplication> findByClubUuid(UUID clubUuid);
|
||||
Mono<Long> countByEventUuid(UUID eventUuid);
|
||||
|
||||
Flux<EventApplication> findByStatus(String status);
|
||||
Flux<EventApplication> findByEventUuidAndStatus(UUID eventUuid, String status);
|
||||
Flux<EventApplication> findByClubUuidAndStatus(UUID clubUuid, String status);
|
||||
|
||||
Mono<Long> countByStatus(String status);
|
||||
Mono<Long> countByEventUuidAndStatus(UUID eventUuid, String status);
|
||||
Mono<Long> countByClubUuidAndStatus(UUID clubUuid, String status);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.example.dateplanner.repositories;
|
||||
|
||||
import com.example.dateplanner.models.entities.TennisClub;
|
||||
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
public interface TennisClubRepository extends ReactiveCrudRepository<TennisClub, java.util.UUID> {
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.example.dateplanner.repositories;
|
||||
|
||||
import com.example.dateplanner.models.entities.TennisEvent;
|
||||
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
public interface TennisEventRepository extends ReactiveCrudRepository<TennisEvent, java.util.UUID> {
|
||||
reactor.core.publisher.Flux<TennisEvent> findByClubUuid(java.util.UUID clubUuid);
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
package com.example.dateplanner.services;
|
||||
|
||||
import com.example.dateplanner.dto.SubmitApplicationDto;
|
||||
import com.example.dateplanner.models.entities.EventApplication;
|
||||
import com.example.dateplanner.repositories.EventApplicationRepository;
|
||||
import com.example.dateplanner.repositories.TennisEventRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.UUID;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ApplicationService {
|
||||
|
||||
private final TurnstileService turnstileService;
|
||||
private final TennisEventRepository eventRepository;
|
||||
private final EventApplicationRepository applicationRepository;
|
||||
private final EmailService emailService;
|
||||
|
||||
// ── Подача заявки ─────────────────────────────────────────────────────────
|
||||
|
||||
public Mono<EventApplication> submit(UUID eventId, SubmitApplicationDto dto) {
|
||||
return turnstileService.verify(dto.getCfTurnstileResponse())
|
||||
.flatMap(valid -> {
|
||||
if (!valid) {
|
||||
return Mono.error(new ResponseStatusException(
|
||||
HttpStatus.BAD_REQUEST, "Капча не пройдена. Попробуйте снова."));
|
||||
}
|
||||
return eventRepository.findById(eventId);
|
||||
})
|
||||
.switchIfEmpty(Mono.error(new ResponseStatusException(
|
||||
HttpStatus.NOT_FOUND, "Мероприятие не найдено")))
|
||||
.flatMap(event -> {
|
||||
if (Boolean.FALSE.equals(event.getRegistrationOpen())) {
|
||||
return Mono.error(new ResponseStatusException(
|
||||
HttpStatus.FORBIDDEN, "Регистрация на это мероприятие закрыта"));
|
||||
}
|
||||
EventApplication app = new EventApplication();
|
||||
app.setEventUuid(event.getUuid());
|
||||
app.setClubUuid(event.getClubUuid());
|
||||
app.setFullName(dto.getFullName());
|
||||
app.setEmail(dto.getEmail());
|
||||
if (dto.getBirthDate() != null && !dto.getBirthDate().isBlank()) {
|
||||
try {
|
||||
app.setBirthDate(LocalDate.parse(dto.getBirthDate()));
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
app.setGender(dto.getGender());
|
||||
app.setPhone(dto.getPhone());
|
||||
app.setRatingPoints(dto.getRatingPoints());
|
||||
app.setStatus("PENDING");
|
||||
app.setCreatedAt(LocalDateTime.now());
|
||||
app.setUpdatedAt(LocalDateTime.now());
|
||||
|
||||
return applicationRepository.save(app)
|
||||
.flatMap(saved ->
|
||||
sendConfirmationEmail(saved, event.getTitle())
|
||||
.thenReturn(saved));
|
||||
});
|
||||
}
|
||||
|
||||
// ── Одобрение ─────────────────────────────────────────────────────────────
|
||||
|
||||
public Mono<EventApplication> approve(UUID appId) {
|
||||
return applicationRepository.findById(appId)
|
||||
.switchIfEmpty(Mono.error(new ResponseStatusException(HttpStatus.NOT_FOUND, "Заявка не найдена")))
|
||||
.flatMap(app -> {
|
||||
if ("APPROVED".equals(app.getStatus())) {
|
||||
return Mono.just(app);
|
||||
}
|
||||
app.setStatus("APPROVED");
|
||||
app.setUpdatedAt(LocalDateTime.now());
|
||||
return applicationRepository.save(app);
|
||||
})
|
||||
.flatMap(saved ->
|
||||
eventRepository.findById(saved.getEventUuid())
|
||||
.flatMap(event -> {
|
||||
int current = event.getCurrentParticipants() != null
|
||||
? event.getCurrentParticipants() : 0;
|
||||
event.setCurrentParticipants(current + 1);
|
||||
return eventRepository.save(event).map(e -> e.getTitle());
|
||||
})
|
||||
.defaultIfEmpty("Мероприятие")
|
||||
.flatMap(title -> sendApprovalEmail(saved, title).thenReturn(saved))
|
||||
);
|
||||
}
|
||||
|
||||
// ── Отклонение ────────────────────────────────────────────────────────────
|
||||
|
||||
public Mono<EventApplication> reject(UUID appId, String reason) {
|
||||
return applicationRepository.findById(appId)
|
||||
.switchIfEmpty(Mono.error(new ResponseStatusException(HttpStatus.NOT_FOUND, "Заявка не найдена")))
|
||||
.flatMap(app -> {
|
||||
app.setStatus("REJECTED");
|
||||
app.setRejectionReason(reason);
|
||||
app.setUpdatedAt(LocalDateTime.now());
|
||||
return applicationRepository.save(app);
|
||||
})
|
||||
.flatMap(saved ->
|
||||
eventRepository.findById(saved.getEventUuid())
|
||||
.map(e -> e.getTitle())
|
||||
.defaultIfEmpty("Мероприятие")
|
||||
.flatMap(title -> sendRejectionEmail(saved, title, reason).thenReturn(saved))
|
||||
);
|
||||
}
|
||||
|
||||
// ── Письма ────────────────────────────────────────────────────────────────
|
||||
|
||||
private Mono<Void> sendConfirmationEmail(EventApplication app, String eventTitle) {
|
||||
String subject = "Заявка на участие в мероприятии «" + eventTitle + "» принята";
|
||||
String birthDateStr = app.getBirthDate() != null
|
||||
? app.getBirthDate().format(DateTimeFormatter.ofPattern("dd.MM.yyyy"))
|
||||
: "не указана";
|
||||
|
||||
String body = """
|
||||
<div style="font-family:sans-serif;max-width:600px;margin:0 auto">
|
||||
<div style="background:linear-gradient(135deg,#2ecc71,#27ae60);padding:30px;border-radius:12px 12px 0 0;text-align:center">
|
||||
<h1 style="color:#fff;margin:0;font-size:1.8rem">TennisHub</h1>
|
||||
<p style="color:rgba(255,255,255,0.9);margin:6px 0 0">Заявка на участие принята</p>
|
||||
</div>
|
||||
<div style="background:#fff;padding:30px;border:1px solid #e0e0e0;border-radius:0 0 12px 12px">
|
||||
<p style="font-size:1.1rem">Уважаемый(ая) <b>%s</b>,</p>
|
||||
<p>Ваша заявка на участие в мероприятии <b>«%s»</b> успешно зарегистрирована и ожидает рассмотрения организаторами.</p>
|
||||
<div style="background:#f8f9fa;border-radius:8px;padding:20px;margin:20px 0">
|
||||
<h3 style="margin:0 0 12px;color:#2c3e50">Данные заявки:</h3>
|
||||
<table style="width:100%%">
|
||||
<tr><td style="color:#666;padding:4px 0">ФИО:</td><td style="font-weight:600">%s</td></tr>
|
||||
<tr><td style="color:#666;padding:4px 0">Email:</td><td>%s</td></tr>
|
||||
<tr><td style="color:#666;padding:4px 0">Телефон:</td><td>%s</td></tr>
|
||||
<tr><td style="color:#666;padding:4px 0">Дата рождения:</td><td>%s</td></tr>
|
||||
<tr><td style="color:#666;padding:4px 0">Пол:</td><td>%s</td></tr>
|
||||
<tr><td style="color:#666;padding:4px 0">Рейтинговые очки:</td><td>%s</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
<p style="color:#666;font-size:0.9rem">Организаторы свяжутся с вами по указанному email или телефону.</p>
|
||||
</div>
|
||||
</div>
|
||||
""".formatted(
|
||||
app.getFullName(), eventTitle,
|
||||
app.getFullName(), app.getEmail(),
|
||||
app.getPhone() != null ? app.getPhone() : "не указан",
|
||||
birthDateStr,
|
||||
app.getGender() != null ? app.getGender() : "не указан",
|
||||
app.getRatingPoints() != null ? app.getRatingPoints() : "не указаны"
|
||||
);
|
||||
|
||||
return emailService.send(app.getEmail(), subject, body)
|
||||
.doOnError(e -> log.error("Failed to send confirmation email to {}: {}", app.getEmail(), e.getMessage()))
|
||||
.onErrorResume(e -> Mono.empty());
|
||||
}
|
||||
|
||||
private Mono<Void> sendApprovalEmail(EventApplication app, String eventTitle) {
|
||||
String subject = "Ваша заявка на участие в «" + eventTitle + "» одобрена!";
|
||||
String body = """
|
||||
<div style="font-family:sans-serif;max-width:600px;margin:0 auto">
|
||||
<div style="background:linear-gradient(135deg,#2ecc71,#27ae60);padding:30px;border-radius:12px 12px 0 0;text-align:center">
|
||||
<h1 style="color:#fff;margin:0;font-size:1.8rem">TennisHub</h1>
|
||||
<p style="color:rgba(255,255,255,0.9);margin:6px 0 0">Заявка одобрена</p>
|
||||
</div>
|
||||
<div style="background:#fff;padding:30px;border:1px solid #e0e0e0;border-radius:0 0 12px 12px">
|
||||
<div style="text-align:center;margin-bottom:24px">
|
||||
<div style="width:64px;height:64px;background:#d1f2e8;border-radius:50%%;display:inline-flex;align-items:center;justify-content:center;font-size:2rem">✅</div>
|
||||
</div>
|
||||
<p style="font-size:1.1rem">Уважаемый(ая) <b>%s</b>,</p>
|
||||
<p>Поздравляем! Ваша заявка на участие в мероприятии <b>«%s»</b> была <b style="color:#27ae60">одобрена</b> организаторами.</p>
|
||||
<p>Ждём вас на мероприятии! Если у вас возникнут вопросы, свяжитесь с организаторами.</p>
|
||||
<p style="color:#666;font-size:0.9rem;margin-top:20px">С уважением,<br>Команда TennisHub</p>
|
||||
</div>
|
||||
</div>
|
||||
""".formatted(app.getFullName(), eventTitle);
|
||||
|
||||
return emailService.send(app.getEmail(), subject, body)
|
||||
.doOnError(e -> log.error("Failed to send approval email to {}: {}", app.getEmail(), e.getMessage()))
|
||||
.onErrorResume(e -> Mono.empty());
|
||||
}
|
||||
|
||||
private Mono<Void> sendRejectionEmail(EventApplication app, String eventTitle, String reason) {
|
||||
String subject = "Решение по заявке на участие в «" + eventTitle + "»";
|
||||
String reasonBlock = (reason != null && !reason.isBlank())
|
||||
? "<div style=\"background:#fff3f3;border-left:4px solid #e74c3c;border-radius:4px;padding:14px 18px;margin:16px 0\">"
|
||||
+ "<b>Причина:</b> " + reason + "</div>"
|
||||
: "";
|
||||
String body = """
|
||||
<div style="font-family:sans-serif;max-width:600px;margin:0 auto">
|
||||
<div style="background:linear-gradient(135deg,#c0392b,#e74c3c);padding:30px;border-radius:12px 12px 0 0;text-align:center">
|
||||
<h1 style="color:#fff;margin:0;font-size:1.8rem">TennisHub</h1>
|
||||
<p style="color:rgba(255,255,255,0.9);margin:6px 0 0">Решение по заявке</p>
|
||||
</div>
|
||||
<div style="background:#fff;padding:30px;border:1px solid #e0e0e0;border-radius:0 0 12px 12px">
|
||||
<div style="text-align:center;margin-bottom:24px">
|
||||
<div style="width:64px;height:64px;background:#fde8e8;border-radius:50%%;display:inline-flex;align-items:center;justify-content:center;font-size:2rem">❌</div>
|
||||
</div>
|
||||
<p style="font-size:1.1rem">Уважаемый(ая) <b>%s</b>,</p>
|
||||
<p>К сожалению, ваша заявка на участие в мероприятии <b>«%s»</b> была <b style="color:#e74c3c">отклонена</b> организаторами.</p>
|
||||
%s
|
||||
<p>Если у вас есть вопросы, пожалуйста, свяжитесь с организаторами напрямую.</p>
|
||||
<p style="color:#666;font-size:0.9rem;margin-top:20px">С уважением,<br>Команда TennisHub</p>
|
||||
</div>
|
||||
</div>
|
||||
""".formatted(app.getFullName(), eventTitle, reasonBlock);
|
||||
|
||||
return emailService.send(app.getEmail(), subject, body)
|
||||
.doOnError(e -> log.error("Failed to send rejection email to {}: {}", app.getEmail(), e.getMessage()))
|
||||
.onErrorResume(e -> Mono.empty());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.example.dateplanner.services;
|
||||
|
||||
import jakarta.mail.MessagingException;
|
||||
import jakarta.mail.internet.MimeMessage;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.mail.javamail.JavaMailSender;
|
||||
import org.springframework.mail.javamail.MimeMessageHelper;
|
||||
import org.springframework.stereotype.Service;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class EmailService {
|
||||
|
||||
@Value("${app.system.email}")
|
||||
private String fromEmail;
|
||||
|
||||
private final JavaMailSender mailSender;
|
||||
|
||||
public Mono<Void> send(String toEmail, String subject, String htmlBody) {
|
||||
return Mono.fromCallable(() -> {
|
||||
MimeMessage message = mailSender.createMimeMessage();
|
||||
MimeMessageHelper helper = new MimeMessageHelper(message, false, "utf-8");
|
||||
helper.setFrom(fromEmail);
|
||||
helper.setTo(toEmail);
|
||||
helper.setSubject(subject);
|
||||
helper.setText(htmlBody, true);
|
||||
mailSender.send(message);
|
||||
return true;
|
||||
})
|
||||
.subscribeOn(Schedulers.boundedElastic())
|
||||
.doOnError(e -> log.error("Не удалось отправить письмо на {}: {}", toEmail, e.getMessage()))
|
||||
.then();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package com.example.dateplanner.services;
|
||||
|
||||
import com.example.dateplanner.utils.PasswordGenerator;
|
||||
import io.jsonwebtoken.Claims;
|
||||
import io.jsonwebtoken.JwtException;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import io.jsonwebtoken.SignatureAlgorithm;
|
||||
import io.jsonwebtoken.security.Keys;
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Date;
|
||||
|
||||
@Slf4j
|
||||
@Getter
|
||||
@Service
|
||||
public class JwtService {
|
||||
private final long accessExpiration;
|
||||
private final long refreshExpiration;
|
||||
private final SecretKey key;
|
||||
|
||||
public JwtService(@Value("${jwt.secret}") String secret, @Value("${jwt.expiration.access}") long accessExpiration, @Value("${jwt.expiration.refresh}") long refreshExpiration){
|
||||
this.accessExpiration = accessExpiration;
|
||||
this.refreshExpiration = refreshExpiration;
|
||||
String secretKey = PasswordGenerator.generatePassword(64, PasswordGenerator.Complexity.HARD);
|
||||
key = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
public String generateAccessToken(String username, String digitalSignature){
|
||||
return Jwts.builder()
|
||||
.setSubject(username)
|
||||
.setIssuedAt(new Date(System.currentTimeMillis()))
|
||||
.setExpiration(new Date(System.currentTimeMillis() + accessExpiration))
|
||||
.signWith(key, SignatureAlgorithm.HS512)
|
||||
.claim("digital_signature",digitalSignature)
|
||||
.compact();
|
||||
}
|
||||
|
||||
public String generateRefreshToken(String username, String digitalSignature) {
|
||||
return Jwts.builder()
|
||||
.setSubject(username)
|
||||
.setIssuedAt(new Date(System.currentTimeMillis()))
|
||||
.setExpiration(new Date(System.currentTimeMillis() + refreshExpiration))
|
||||
.signWith(key, SignatureAlgorithm.HS512)
|
||||
.claim("digital_signature",digitalSignature)
|
||||
.compact();
|
||||
}
|
||||
|
||||
public boolean validateToken(String token) {
|
||||
try {
|
||||
Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
|
||||
return true;
|
||||
} catch (JwtException | IllegalArgumentException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public String getUsernameFromToken(String token) {
|
||||
try {
|
||||
Claims claims = Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
|
||||
return claims.getSubject();
|
||||
} catch (JwtException | IllegalArgumentException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public String getDigitalSignatureFromToken(String token) {
|
||||
try {
|
||||
Claims claims = Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
|
||||
if (claims.containsKey("digital_signature")) {
|
||||
return (String) claims.get("digital_signature");
|
||||
} else {
|
||||
throw new IllegalArgumentException("Token does not contain digital_signature claim");
|
||||
}
|
||||
} catch (JwtException | IllegalArgumentException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.example.dateplanner.services;
|
||||
|
||||
import com.example.dateplanner.utils.InputStreamCollector;
|
||||
import io.minio.*;
|
||||
import io.minio.errors.*;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.io.FilenameUtils;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.codec.multipart.FilePart;
|
||||
import org.springframework.stereotype.Service;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class MinioService {
|
||||
|
||||
private final MinioClient minioClient;
|
||||
private final String bucket;
|
||||
|
||||
@SneakyThrows
|
||||
public MinioService(MinioClient minioClient, @Value("${minio.bucket}") String bucket) {
|
||||
this.minioClient = minioClient;
|
||||
this.bucket = bucket;
|
||||
if (!this.minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucket).build())) {
|
||||
this.minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucket).build());
|
||||
log.info("MinIO bucket '{}' created", bucket);
|
||||
}
|
||||
}
|
||||
|
||||
public Mono<String> uploadImage(FilePart file) {
|
||||
return file.content()
|
||||
.subscribeOn(Schedulers.boundedElastic())
|
||||
.reduce(new InputStreamCollector(),
|
||||
(collector, dataBuffer) -> collector.collectInputStream(dataBuffer.asInputStream()))
|
||||
.flatMap(collector -> {
|
||||
long start = System.currentTimeMillis();
|
||||
String extension = FilenameUtils.getExtension(file.filename());
|
||||
String objectName = UUID.randomUUID() + (extension.isEmpty() ? ".jpg" : "." + extension);
|
||||
try {
|
||||
String contentType = Objects.requireNonNull(file.headers().getContentType()).toString();
|
||||
PutObjectArgs args = PutObjectArgs.builder()
|
||||
.bucket(bucket)
|
||||
.object(objectName)
|
||||
.contentType(contentType)
|
||||
.stream(collector.getStream(), collector.getTotalSize(), -1)
|
||||
.build();
|
||||
minioClient.putObject(args);
|
||||
collector.closeStream();
|
||||
log.info("MinIO upload '{}' in {} ms", objectName, System.currentTimeMillis() - start);
|
||||
return Mono.just("/" + bucket + "/" + objectName);
|
||||
} catch (Exception e) {
|
||||
log.error("MinIO upload failed: {}", e.getMessage());
|
||||
return Mono.error(new RuntimeException(e));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package com.example.dateplanner.services;
|
||||
|
||||
import com.example.dateplanner.models.entities.AppUser;
|
||||
import com.example.dateplanner.models.enums.Role;
|
||||
import com.example.dateplanner.repositories.AppUserRepository;
|
||||
import com.example.dateplanner.utils.PasswordGenerator;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class RegistrationService {
|
||||
|
||||
private final AppUserRepository userRepository;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
private final EmailService emailService;
|
||||
|
||||
public Mono<Void> register(String email) {
|
||||
return userRepository.findByPhone(email)
|
||||
.flatMap(existing -> Mono.<Void>error(
|
||||
new IllegalArgumentException("Этот email уже зарегистрирован")))
|
||||
.switchIfEmpty(Mono.defer(() -> createUser(email)));
|
||||
}
|
||||
|
||||
private Mono<Void> createUser(String email) {
|
||||
String rawPassword = PasswordGenerator.generatePassword(10, PasswordGenerator.Complexity.MEDIUM);
|
||||
|
||||
AppUser user = new AppUser();
|
||||
user.setPhone(email);
|
||||
user.setPassword(passwordEncoder.encode(rawPassword));
|
||||
user.setFirstName("");
|
||||
user.setLastName("");
|
||||
user.setRole(Role.USER);
|
||||
user.setEnabled(true);
|
||||
user.setCreatedAt(LocalDateTime.now());
|
||||
user.setUpdatedAt(LocalDateTime.now());
|
||||
|
||||
return userRepository.save(user)
|
||||
.flatMap(saved -> {
|
||||
log.info("Зарегистрирован новый пользователь: {}", email);
|
||||
return emailService.send(email, "Регистрация на TennisHub", buildWelcomeEmail(email, rawPassword));
|
||||
})
|
||||
.onErrorResume(e -> {
|
||||
log.error("Ошибка при отправке письма на {}: {}", email, e.getMessage());
|
||||
return userRepository.findByPhone(email)
|
||||
.flatMap(userRepository::delete)
|
||||
.then(Mono.error(new IllegalStateException("Не удалось отправить письмо. Проверьте адрес почты.")));
|
||||
})
|
||||
.then();
|
||||
}
|
||||
|
||||
private String buildWelcomeEmail(String email, String password) {
|
||||
return "<!DOCTYPE html>" +
|
||||
"<html><head><meta charset='utf-8'>" +
|
||||
"<title>Добро пожаловать на TennisHub</title></head>" +
|
||||
"<body style='margin:0;padding:0;background:#f4f4f4;font-family:Arial,sans-serif;'>" +
|
||||
"<div style='max-width:600px;margin:30px auto;background:#fff;border-radius:10px;overflow:hidden;box-shadow:0 4px 12px rgba(0,0,0,0.1);'>" +
|
||||
|
||||
" <div style='background:#2c3e50;padding:30px;text-align:center;'>" +
|
||||
" <span style='font-size:2rem;font-weight:800;'>" +
|
||||
" <span style='color:#2ecc71;'>Tennis</span><span style='color:#fff;'>Hub</span>" +
|
||||
" </span>" +
|
||||
" </div>" +
|
||||
|
||||
" <div style='padding:35px 40px;'>" +
|
||||
" <h2 style='color:#2c3e50;margin-top:0;'>Добро пожаловать!</h2>" +
|
||||
" <p style='color:#555;line-height:1.6;'>Вы успешно зарегистрировались на платформе TennisHub." +
|
||||
" Ниже ваши данные для входа:</p>" +
|
||||
|
||||
" <div style='background:#f8f9fa;border-left:4px solid #2ecc71;border-radius:4px;padding:20px 25px;margin:25px 0;'>" +
|
||||
" <p style='margin:0 0 10px;color:#333;'><strong>Логин (email):</strong> " + email + "</p>" +
|
||||
" <p style='margin:0;color:#333;'><strong>Пароль:</strong> " + password + "</p>" +
|
||||
" </div>" +
|
||||
|
||||
" <p style='color:#555;line-height:1.6;'>Рекомендуем сменить пароль после первого входа.</p>" +
|
||||
|
||||
" <div style='background:#fff8e1;border-left:4px solid #e67e22;border-radius:4px;padding:15px 20px;margin-top:20px;'>" +
|
||||
" <p style='margin:0;color:#555;font-size:13px;'>" +
|
||||
" <strong>Обратите внимание:</strong> не отвечайте на это письмо." +
|
||||
" </p>" +
|
||||
" </div>" +
|
||||
" </div>" +
|
||||
|
||||
" <div style='background:#f4f4f4;padding:15px;text-align:center;color:#999;font-size:12px;'>" +
|
||||
" © " + java.time.Year.now().getValue() + " TennisHub. Все права защищены." +
|
||||
" </div>" +
|
||||
"</div></body></html>";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.example.dateplanner.services;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.reactive.function.BodyInserters;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class TurnstileService {
|
||||
|
||||
@Value("${turnstile.secret-key}")
|
||||
private String secretKey;
|
||||
|
||||
@Value("${turnstile.enabled:true}")
|
||||
private boolean enabled;
|
||||
|
||||
private final WebClient webClient = WebClient.builder()
|
||||
.baseUrl("https://challenges.cloudflare.com")
|
||||
.build();
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public Mono<Boolean> verify(String token) {
|
||||
if (!enabled) {
|
||||
return Mono.just(true);
|
||||
}
|
||||
if (token == null || token.isBlank()) {
|
||||
return Mono.just(false);
|
||||
}
|
||||
return webClient.post()
|
||||
.uri("/turnstile/v0/siteverify")
|
||||
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
|
||||
.body(BodyInserters.fromFormData("secret", secretKey).with("response", token))
|
||||
.retrieve()
|
||||
.bodyToMono(Map.class)
|
||||
.map(body -> Boolean.TRUE.equals(body.get("success")))
|
||||
.onErrorResume(e -> {
|
||||
log.error("Turnstile verification error: {}", e.getMessage());
|
||||
return Mono.just(false);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.example.dateplanner.services;
|
||||
|
||||
import com.example.dateplanner.models.entities.AppUser;
|
||||
import com.example.dateplanner.repositories.AppUserRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class UserService implements ReactiveUserDetailsService {
|
||||
private final AppUserRepository userRepository;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
|
||||
@Override
|
||||
public Mono<UserDetails> findByUsername(String username) {
|
||||
log.info("username {}", username);
|
||||
return userRepository.findByPhone(username).flatMap(Mono::just).cast(UserDetails.class);
|
||||
}
|
||||
|
||||
public Mono<AppUser> getByUuid(UUID uuid) {
|
||||
return userRepository.findById(uuid);
|
||||
}
|
||||
|
||||
public Mono<AppUser> getUserByUuid(UUID uuid) {
|
||||
return userRepository.findById(uuid);
|
||||
}
|
||||
}
|
||||
27
src/main/java/com/example/dateplanner/utils/CookieUtil.java
Normal file
27
src/main/java/com/example/dateplanner/utils/CookieUtil.java
Normal file
@@ -0,0 +1,27 @@
|
||||
package com.example.dateplanner.utils;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
public class CookieUtil {
|
||||
private static CookieUtil instance = null;
|
||||
|
||||
private final String REFRESH;
|
||||
private final String ACCESS;
|
||||
private final String SESSION;
|
||||
|
||||
protected CookieUtil(){
|
||||
String appName = "dp";
|
||||
REFRESH = appName + "-refresh";
|
||||
ACCESS = appName + "-access";
|
||||
SESSION = appName + "-session";
|
||||
}
|
||||
|
||||
public static CookieUtil getInstance(){
|
||||
if(instance == null){
|
||||
instance = new CookieUtil();
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.example.dateplanner.utils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.SequenceInputStream;
|
||||
|
||||
public class InputStreamCollector {
|
||||
private InputStream inputStream;
|
||||
private long totalSize = 0;
|
||||
|
||||
public InputStreamCollector collectInputStream(InputStream is) {
|
||||
try {
|
||||
int available = is.available();
|
||||
totalSize += available;
|
||||
} catch (IOException ignored) {}
|
||||
|
||||
if (this.inputStream == null) {
|
||||
this.inputStream = is;
|
||||
} else {
|
||||
this.inputStream = new SequenceInputStream(this.inputStream, is);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public InputStream getStream() {
|
||||
return inputStream;
|
||||
}
|
||||
|
||||
public long getTotalSize() {
|
||||
return totalSize;
|
||||
}
|
||||
|
||||
public void closeStream() throws IOException {
|
||||
if (inputStream != null) {
|
||||
inputStream.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.example.dateplanner.utils;
|
||||
|
||||
import java.util.Random;
|
||||
|
||||
public class PasswordGenerator {
|
||||
private static final String LOWERCASE_LETTERS = "abcdefghijklmnopqrstuvwxyz";
|
||||
private static final String UPPERCASE_LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
private static final String NUMBERS = "0123456789";
|
||||
private static final String SPECIAL_CHARACTERS = "!@#$%^&*()-_=+[]{}|;:,.<>?";
|
||||
|
||||
private static final String EASY_CHARACTERS = LOWERCASE_LETTERS + UPPERCASE_LETTERS;
|
||||
private static final String MEDIUM_CHARACTERS = EASY_CHARACTERS + NUMBERS;
|
||||
private static final String HARD_CHARACTERS = MEDIUM_CHARACTERS + SPECIAL_CHARACTERS;
|
||||
|
||||
private static final Random RANDOM = new Random();
|
||||
|
||||
public enum Complexity {
|
||||
EASY, MEDIUM, HARD
|
||||
}
|
||||
|
||||
public static String generatePassword(int length, Complexity complexity) {
|
||||
if (length <= 0) {
|
||||
throw new IllegalArgumentException("Password length must be greater than zero");
|
||||
}
|
||||
|
||||
StringBuilder password = new StringBuilder(length);
|
||||
String characters;
|
||||
|
||||
switch (complexity) {
|
||||
case EASY:
|
||||
characters = EASY_CHARACTERS;
|
||||
break;
|
||||
case MEDIUM:
|
||||
characters = MEDIUM_CHARACTERS;
|
||||
break;
|
||||
case HARD:
|
||||
characters = HARD_CHARACTERS;
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Invalid complexity level");
|
||||
}
|
||||
|
||||
for (int i = 0; i < length; i++) {
|
||||
password.append(characters.charAt(RANDOM.nextInt(characters.length())));
|
||||
}
|
||||
|
||||
return password.toString();
|
||||
}
|
||||
}
|
||||
@@ -28,3 +28,22 @@ minio.url=
|
||||
minio.cdn=
|
||||
minio.username=
|
||||
minio.password=
|
||||
#========================== email config =========================
|
||||
app.system.email=tennisworld.kids@gmail.com
|
||||
app.system.email.password=oypectvsvyszlzqb
|
||||
app.system.email.port=465
|
||||
app.system.email.server=smtp.gmail.com
|
||||
|
||||
spring.mail.host=${app.system.email.server}
|
||||
spring.mail.port=${app.system.email.port}
|
||||
spring.mail.username=${app.system.email}
|
||||
spring.mail.password=${app.system.email.password}
|
||||
spring.mail.protocol=smtps
|
||||
spring.mail.properties.mail.smtp.auth=true
|
||||
spring.mail.properties.mail.smtp.socketFactory.port=${app.system.email.port}
|
||||
spring.mail.properties.mail.smtp.socketFactory.class=javax.net.ssl.SSLSocketFactory
|
||||
spring.mail.properties.mail.smtp.socketFactory.fallback=false
|
||||
#====================== turnstile config ========================
|
||||
turnstile.site-key=1x00000000000000000000AA
|
||||
turnstile.secret-key=1x0000000000000000000000000000000AA
|
||||
turnstile.enabled=true
|
||||
174
src/main/resources/db/migration/V1_0_1__init.sql
Normal file
174
src/main/resources/db/migration/V1_0_1__init.sql
Normal file
@@ -0,0 +1,174 @@
|
||||
-- V2__create_tables.sql
|
||||
|
||||
-- Расширение для генерации UUID
|
||||
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
|
||||
|
||||
-- Функция для автоматического обновления поля updated_at
|
||||
CREATE OR REPLACE FUNCTION update_updated_at_column()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = CURRENT_TIMESTAMP;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ language 'plpgsql';
|
||||
|
||||
-- =====================================================
|
||||
-- Таблица app_users
|
||||
-- =====================================================
|
||||
CREATE TABLE app_users (
|
||||
uuid UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
phone TEXT NOT NULL UNIQUE,
|
||||
password TEXT,
|
||||
last_name TEXT NOT NULL,
|
||||
first_name TEXT NOT NULL,
|
||||
role TEXT NOT NULL DEFAULT 'USER',
|
||||
enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
COMMENT ON TABLE app_users IS 'Пользователи приложения';
|
||||
COMMENT ON COLUMN app_users.role IS 'Роль пользователя: USER, ADMIN, MODERATOR';
|
||||
COMMENT ON COLUMN app_users.enabled IS 'Активен ли аккаунт';
|
||||
|
||||
-- =====================================================
|
||||
-- Таблица tennis_clubs
|
||||
-- =====================================================
|
||||
CREATE TABLE tennis_clubs (
|
||||
uuid UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
address TEXT,
|
||||
phone TEXT,
|
||||
email TEXT,
|
||||
website TEXT,
|
||||
images TEXT[] DEFAULT '{}',
|
||||
court_uuids UUID[] DEFAULT '{}',
|
||||
conveniences TEXT,
|
||||
owner_uuid UUID,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
COMMENT ON TABLE tennis_clubs IS 'Теннисные клубы';
|
||||
COMMENT ON COLUMN tennis_clubs.images IS 'Массив ссылок на изображения клуба';
|
||||
COMMENT ON COLUMN tennis_clubs.court_uuids IS 'Массив UUID кортов, принадлежащих клубу';
|
||||
COMMENT ON COLUMN tennis_clubs.conveniences IS 'Удобства клуба (раздевалки, душ, кафе и т.д.)';
|
||||
|
||||
-- =====================================================
|
||||
-- Таблица courts
|
||||
-- =====================================================
|
||||
CREATE TABLE courts (
|
||||
uuid UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name TEXT NOT NULL,
|
||||
photo TEXT,
|
||||
price INTEGER NOT NULL,
|
||||
cover TEXT,
|
||||
is_outdoor BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
is_visible BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
COMMENT ON TABLE courts IS 'Теннисные корты';
|
||||
COMMENT ON COLUMN courts.price IS 'Цена за час аренды в рублях';
|
||||
COMMENT ON COLUMN courts.cover IS 'Тип покрытия (грунт, хард, трава)';
|
||||
COMMENT ON COLUMN courts.is_outdoor IS 'Открытый корт (true) или крытый (false)';
|
||||
COMMENT ON COLUMN courts.is_visible IS 'Виден ли корт для бронирования';
|
||||
|
||||
-- =====================================================
|
||||
-- Таблица tennis_events
|
||||
-- =====================================================
|
||||
CREATE TABLE tennis_events (
|
||||
uuid UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
club_uuid UUID NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
description TEXT,
|
||||
type TEXT NOT NULL,
|
||||
start_datetime TIMESTAMP NOT NULL,
|
||||
end_datetime TIMESTAMP,
|
||||
max_participants INTEGER,
|
||||
current_participants INTEGER NOT NULL DEFAULT 0,
|
||||
take_part_price DECIMAL(10, 2),
|
||||
rewards TEXT,
|
||||
requirements TEXT,
|
||||
contact_email TEXT,
|
||||
contact_phone TEXT,
|
||||
image_url TEXT,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
-- Внешний ключ
|
||||
CONSTRAINT fk_tennis_events_club FOREIGN KEY (club_uuid)
|
||||
REFERENCES tennis_clubs(uuid) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
COMMENT ON TABLE tennis_events IS 'Теннисные мероприятия (тренировки, турниры, игры)';
|
||||
COMMENT ON COLUMN tennis_events.type IS 'Тип мероприятия: TRAINING, TOURNAMENT, FRIENDLY_MATCH, MASTER_CLASS, OPEN_COURT';
|
||||
COMMENT ON COLUMN tennis_events.max_participants IS 'Максимальное количество участников';
|
||||
COMMENT ON COLUMN tennis_events.current_participants IS 'Текущее количество участников';
|
||||
COMMENT ON COLUMN tennis_events.take_part_price IS 'Цена участия';
|
||||
COMMENT ON COLUMN tennis_events.rewards IS 'Призы и награды';
|
||||
COMMENT ON COLUMN tennis_events.requirements IS 'Требования к участникам (уровень игры, экипировка и т.д.)';
|
||||
|
||||
-- =====================================================
|
||||
-- Триггеры для обновления updated_at
|
||||
-- =====================================================
|
||||
CREATE TRIGGER update_app_users_updated_at
|
||||
BEFORE UPDATE ON app_users
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
CREATE TRIGGER update_tennis_clubs_updated_at
|
||||
BEFORE UPDATE ON tennis_clubs
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
CREATE TRIGGER update_courts_updated_at
|
||||
BEFORE UPDATE ON courts
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
CREATE TRIGGER update_tennis_events_updated_at
|
||||
BEFORE UPDATE ON tennis_events
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
-- =====================================================
|
||||
-- Индексы для производительности
|
||||
-- =====================================================
|
||||
|
||||
-- Индексы для app_users
|
||||
CREATE INDEX idx_app_users_phone ON app_users(phone);
|
||||
CREATE INDEX idx_app_users_role ON app_users(role);
|
||||
CREATE INDEX idx_app_users_enabled ON app_users(enabled);
|
||||
|
||||
-- Индексы для tennis_clubs
|
||||
CREATE INDEX idx_tennis_clubs_name ON tennis_clubs(name);
|
||||
CREATE INDEX idx_tennis_clubs_address ON tennis_clubs(address);
|
||||
CREATE INDEX idx_tennis_clubs_email ON tennis_clubs(email);
|
||||
CREATE INDEX idx_tennis_clubs_phone ON tennis_clubs(phone);
|
||||
|
||||
-- GIN индекс для поиска по массиву изображений
|
||||
CREATE INDEX idx_tennis_clubs_images ON tennis_clubs USING GIN (images);
|
||||
|
||||
-- GIN индекс для поиска по массиву UUID кортов
|
||||
CREATE INDEX idx_tennis_clubs_court_uuids ON tennis_clubs USING GIN (court_uuids);
|
||||
|
||||
-- Индексы для courts
|
||||
CREATE INDEX idx_courts_name ON courts(name);
|
||||
CREATE INDEX idx_courts_price ON courts(price);
|
||||
CREATE INDEX idx_courts_cover ON courts(cover);
|
||||
CREATE INDEX idx_courts_is_outdoor ON courts(is_outdoor);
|
||||
CREATE INDEX idx_courts_is_visible ON courts(is_visible);
|
||||
|
||||
-- Индексы для tennis_events
|
||||
CREATE INDEX idx_tennis_events_club_uuid ON tennis_events(club_uuid);
|
||||
CREATE INDEX idx_tennis_events_type ON tennis_events(type);
|
||||
CREATE INDEX idx_tennis_events_start_datetime ON tennis_events(start_datetime);
|
||||
CREATE INDEX idx_tennis_events_club_status ON tennis_events(club_uuid, start_datetime);
|
||||
CREATE INDEX idx_tennis_events_start_end ON tennis_events(start_datetime, end_datetime);
|
||||
|
||||
-- =====================================================
|
||||
-- Комментарии к таблицам
|
||||
-- =====================================================
|
||||
COMMENT ON TABLE app_users IS 'Пользователи приложения';
|
||||
COMMENT ON TABLE tennis_clubs IS 'Теннисные клубы';
|
||||
COMMENT ON TABLE courts IS 'Теннисные корты';
|
||||
COMMENT ON TABLE tennis_events IS 'Теннисные мероприятия';
|
||||
@@ -0,0 +1,25 @@
|
||||
CREATE TABLE event_applications (
|
||||
uuid UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
event_uuid UUID NOT NULL,
|
||||
club_uuid UUID NOT NULL,
|
||||
full_name TEXT NOT NULL,
|
||||
email TEXT NOT NULL,
|
||||
birth_date DATE,
|
||||
gender TEXT,
|
||||
phone TEXT,
|
||||
rating_points INTEGER,
|
||||
status TEXT NOT NULL DEFAULT 'PENDING',
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT fk_app_event FOREIGN KEY (event_uuid) REFERENCES tennis_events(uuid) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_app_club FOREIGN KEY (club_uuid) REFERENCES tennis_clubs(uuid) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX idx_event_applications_event_uuid ON event_applications(event_uuid);
|
||||
CREATE INDEX idx_event_applications_club_uuid ON event_applications(club_uuid);
|
||||
CREATE INDEX idx_event_applications_status ON event_applications(status);
|
||||
CREATE INDEX idx_event_applications_created_at ON event_applications(created_at DESC);
|
||||
|
||||
CREATE TRIGGER update_event_applications_updated_at
|
||||
BEFORE UPDATE ON event_applications
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE event_applications ADD COLUMN IF NOT EXISTS rejection_reason TEXT;
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE tennis_events ADD COLUMN IF NOT EXISTS registration_open BOOLEAN NOT NULL DEFAULT TRUE;
|
||||
@@ -0,0 +1,31 @@
|
||||
-- Add club_uuid to courts table
|
||||
ALTER TABLE courts ADD COLUMN IF NOT EXISTS club_uuid UUID;
|
||||
ALTER TABLE courts ADD COLUMN IF NOT EXISTS description TEXT;
|
||||
ALTER TABLE courts ADD CONSTRAINT fk_courts_club
|
||||
FOREIGN KEY (club_uuid) REFERENCES tennis_clubs(uuid) ON DELETE CASCADE;
|
||||
CREATE INDEX IF NOT EXISTS idx_courts_club_uuid ON courts(club_uuid);
|
||||
|
||||
-- Add contact/social fields to tennis_clubs
|
||||
ALTER TABLE tennis_clubs ADD COLUMN IF NOT EXISTS contact_phone TEXT;
|
||||
ALTER TABLE tennis_clubs ADD COLUMN IF NOT EXISTS contact_person TEXT;
|
||||
ALTER TABLE tennis_clubs ADD COLUMN IF NOT EXISTS telegram_link TEXT;
|
||||
|
||||
-- Court booking requests
|
||||
CREATE TABLE IF NOT EXISTS court_booking_requests (
|
||||
uuid UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
club_uuid UUID NOT NULL,
|
||||
court_uuid UUID,
|
||||
court_name TEXT,
|
||||
requester_name TEXT NOT NULL,
|
||||
requester_email TEXT NOT NULL,
|
||||
requester_phone TEXT,
|
||||
booking_date DATE NOT NULL,
|
||||
start_time TIME NOT NULL,
|
||||
end_time TIME NOT NULL,
|
||||
message TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'NEW',
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT fk_booking_club FOREIGN KEY (club_uuid)
|
||||
REFERENCES tennis_clubs(uuid) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_booking_club ON court_booking_requests(club_uuid);
|
||||
@@ -1 +1,12 @@
|
||||
@import "fonts.css";
|
||||
|
||||
.modal-section-label {
|
||||
font-size: 0.68rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
color: #9ba4af;
|
||||
margin-bottom: 10px;
|
||||
padding-bottom: 6px;
|
||||
border-bottom: 1px solid #f0f2f4;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*!
|
||||
* Bootstrap Grid v5.3.3 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2024 The Bootstrap Authors
|
||||
* Bootstrap Grid v5.3.8 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2025 The Bootstrap Authors
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||
*/
|
||||
.container,
|
||||
@@ -73,7 +73,7 @@
|
||||
}
|
||||
|
||||
.col {
|
||||
flex: 1 0 0%;
|
||||
flex: 1 0 0;
|
||||
}
|
||||
|
||||
.row-cols-auto > * {
|
||||
@@ -282,7 +282,7 @@
|
||||
|
||||
@media (min-width: 576px) {
|
||||
.col-sm {
|
||||
flex: 1 0 0%;
|
||||
flex: 1 0 0;
|
||||
}
|
||||
.row-cols-sm-auto > * {
|
||||
flex: 0 0 auto;
|
||||
@@ -451,7 +451,7 @@
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
.col-md {
|
||||
flex: 1 0 0%;
|
||||
flex: 1 0 0;
|
||||
}
|
||||
.row-cols-md-auto > * {
|
||||
flex: 0 0 auto;
|
||||
@@ -620,7 +620,7 @@
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
.col-lg {
|
||||
flex: 1 0 0%;
|
||||
flex: 1 0 0;
|
||||
}
|
||||
.row-cols-lg-auto > * {
|
||||
flex: 0 0 auto;
|
||||
@@ -789,7 +789,7 @@
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
.col-xl {
|
||||
flex: 1 0 0%;
|
||||
flex: 1 0 0;
|
||||
}
|
||||
.row-cols-xl-auto > * {
|
||||
flex: 0 0 auto;
|
||||
@@ -958,7 +958,7 @@
|
||||
}
|
||||
@media (min-width: 1400px) {
|
||||
.col-xxl {
|
||||
flex: 1 0 0%;
|
||||
flex: 1 0 0;
|
||||
}
|
||||
.row-cols-xxl-auto > * {
|
||||
flex: 0 0 auto;
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,6 +1,6 @@
|
||||
/*!
|
||||
* Bootstrap Grid v5.3.3 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2024 The Bootstrap Authors
|
||||
* Bootstrap Grid v5.3.8 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2025 The Bootstrap Authors
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||
*/
|
||||
.container,
|
||||
@@ -73,7 +73,7 @@
|
||||
}
|
||||
|
||||
.col {
|
||||
flex: 1 0 0%;
|
||||
flex: 1 0 0;
|
||||
}
|
||||
|
||||
.row-cols-auto > * {
|
||||
@@ -282,7 +282,7 @@
|
||||
|
||||
@media (min-width: 576px) {
|
||||
.col-sm {
|
||||
flex: 1 0 0%;
|
||||
flex: 1 0 0;
|
||||
}
|
||||
.row-cols-sm-auto > * {
|
||||
flex: 0 0 auto;
|
||||
@@ -451,7 +451,7 @@
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
.col-md {
|
||||
flex: 1 0 0%;
|
||||
flex: 1 0 0;
|
||||
}
|
||||
.row-cols-md-auto > * {
|
||||
flex: 0 0 auto;
|
||||
@@ -620,7 +620,7 @@
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
.col-lg {
|
||||
flex: 1 0 0%;
|
||||
flex: 1 0 0;
|
||||
}
|
||||
.row-cols-lg-auto > * {
|
||||
flex: 0 0 auto;
|
||||
@@ -789,7 +789,7 @@
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
.col-xl {
|
||||
flex: 1 0 0%;
|
||||
flex: 1 0 0;
|
||||
}
|
||||
.row-cols-xl-auto > * {
|
||||
flex: 0 0 auto;
|
||||
@@ -958,7 +958,7 @@
|
||||
}
|
||||
@media (min-width: 1400px) {
|
||||
.col-xxl {
|
||||
flex: 1 0 0%;
|
||||
flex: 1 0 0;
|
||||
}
|
||||
.row-cols-xxl-auto > * {
|
||||
flex: 0 0 auto;
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
13471
src/main/resources/static/js/fw/bootstrap/dist/css/bootstrap-min.css
vendored
Normal file
13471
src/main/resources/static/js/fw/bootstrap/dist/css/bootstrap-min.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
/*!
|
||||
* Bootstrap Reboot v5.3.3 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2024 The Bootstrap Authors
|
||||
* Bootstrap Reboot v5.3.8 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2025 The Bootstrap Authors
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||
*/
|
||||
:root,
|
||||
@@ -516,8 +516,8 @@ legend {
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: calc(1.275rem + 0.3vw);
|
||||
line-height: inherit;
|
||||
font-size: calc(1.275rem + 0.3vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
legend {
|
||||
@@ -546,6 +546,10 @@ legend + * {
|
||||
-webkit-appearance: textfield;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
[type=search]::-webkit-search-cancel-button {
|
||||
cursor: pointer;
|
||||
filter: grayscale(1);
|
||||
}
|
||||
|
||||
/* rtl:raw:
|
||||
[type="tel"],
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,6 +1,6 @@
|
||||
/*!
|
||||
* Bootstrap Reboot v5.3.3 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2024 The Bootstrap Authors
|
||||
* Bootstrap Reboot v5.3.8 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2025 The Bootstrap Authors
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||
*/
|
||||
:root,
|
||||
@@ -516,8 +516,8 @@ legend {
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: calc(1.275rem + 0.3vw);
|
||||
line-height: inherit;
|
||||
font-size: calc(1.275rem + 0.3vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
legend {
|
||||
@@ -546,6 +546,10 @@ legend + * {
|
||||
-webkit-appearance: textfield;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
[type=search]::-webkit-search-cancel-button {
|
||||
cursor: pointer;
|
||||
filter: grayscale(1);
|
||||
}
|
||||
|
||||
[type="tel"],
|
||||
[type="url"],
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,6 +1,6 @@
|
||||
/*!
|
||||
* Bootstrap Utilities v5.3.3 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2024 The Bootstrap Authors
|
||||
* Bootstrap Utilities v5.3.8 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2025 The Bootstrap Authors
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||
*/
|
||||
:root,
|
||||
@@ -519,6 +519,10 @@
|
||||
.visually-hidden-focusable:not(:focus):not(:focus-within):not(caption) {
|
||||
position: absolute !important;
|
||||
}
|
||||
.visually-hidden *,
|
||||
.visually-hidden-focusable:not(:focus):not(:focus-within) * {
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
.stretched-link::after {
|
||||
position: absolute;
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,6 +1,6 @@
|
||||
/*!
|
||||
* Bootstrap Utilities v5.3.3 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2024 The Bootstrap Authors
|
||||
* Bootstrap Utilities v5.3.8 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2025 The Bootstrap Authors
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||
*/
|
||||
:root,
|
||||
@@ -519,6 +519,10 @@
|
||||
.visually-hidden-focusable:not(:focus):not(:focus-within):not(caption) {
|
||||
position: absolute !important;
|
||||
}
|
||||
.visually-hidden *,
|
||||
.visually-hidden-focusable:not(:focus):not(:focus-within) * {
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
.stretched-link::after {
|
||||
position: absolute;
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,7 +1,7 @@
|
||||
@charset "UTF-8";
|
||||
/*!
|
||||
* Bootstrap v5.3.3 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2024 The Bootstrap Authors
|
||||
* Bootstrap v5.3.8 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2025 The Bootstrap Authors
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||
*/
|
||||
:root,
|
||||
@@ -517,8 +517,8 @@ legend {
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: calc(1.275rem + 0.3vw);
|
||||
line-height: inherit;
|
||||
font-size: calc(1.275rem + 0.3vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
legend {
|
||||
@@ -547,6 +547,10 @@ legend + * {
|
||||
-webkit-appearance: textfield;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
[type=search]::-webkit-search-cancel-button {
|
||||
cursor: pointer;
|
||||
filter: grayscale(1);
|
||||
}
|
||||
|
||||
/* rtl:raw:
|
||||
[type="tel"],
|
||||
@@ -601,9 +605,9 @@ progress {
|
||||
}
|
||||
|
||||
.display-1 {
|
||||
font-size: calc(1.625rem + 4.5vw);
|
||||
font-weight: 300;
|
||||
line-height: 1.2;
|
||||
font-size: calc(1.625rem + 4.5vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
.display-1 {
|
||||
@@ -612,9 +616,9 @@ progress {
|
||||
}
|
||||
|
||||
.display-2 {
|
||||
font-size: calc(1.575rem + 3.9vw);
|
||||
font-weight: 300;
|
||||
line-height: 1.2;
|
||||
font-size: calc(1.575rem + 3.9vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
.display-2 {
|
||||
@@ -623,9 +627,9 @@ progress {
|
||||
}
|
||||
|
||||
.display-3 {
|
||||
font-size: calc(1.525rem + 3.3vw);
|
||||
font-weight: 300;
|
||||
line-height: 1.2;
|
||||
font-size: calc(1.525rem + 3.3vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
.display-3 {
|
||||
@@ -634,9 +638,9 @@ progress {
|
||||
}
|
||||
|
||||
.display-4 {
|
||||
font-size: calc(1.475rem + 2.7vw);
|
||||
font-weight: 300;
|
||||
line-height: 1.2;
|
||||
font-size: calc(1.475rem + 2.7vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
.display-4 {
|
||||
@@ -645,9 +649,9 @@ progress {
|
||||
}
|
||||
|
||||
.display-5 {
|
||||
font-size: calc(1.425rem + 2.1vw);
|
||||
font-weight: 300;
|
||||
line-height: 1.2;
|
||||
font-size: calc(1.425rem + 2.1vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
.display-5 {
|
||||
@@ -656,9 +660,9 @@ progress {
|
||||
}
|
||||
|
||||
.display-6 {
|
||||
font-size: calc(1.375rem + 1.5vw);
|
||||
font-weight: 300;
|
||||
line-height: 1.2;
|
||||
font-size: calc(1.375rem + 1.5vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
.display-6 {
|
||||
@@ -803,7 +807,7 @@ progress {
|
||||
}
|
||||
|
||||
.col {
|
||||
flex: 1 0 0%;
|
||||
flex: 1 0 0;
|
||||
}
|
||||
|
||||
.row-cols-auto > * {
|
||||
@@ -1012,7 +1016,7 @@ progress {
|
||||
|
||||
@media (min-width: 576px) {
|
||||
.col-sm {
|
||||
flex: 1 0 0%;
|
||||
flex: 1 0 0;
|
||||
}
|
||||
.row-cols-sm-auto > * {
|
||||
flex: 0 0 auto;
|
||||
@@ -1181,7 +1185,7 @@ progress {
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
.col-md {
|
||||
flex: 1 0 0%;
|
||||
flex: 1 0 0;
|
||||
}
|
||||
.row-cols-md-auto > * {
|
||||
flex: 0 0 auto;
|
||||
@@ -1350,7 +1354,7 @@ progress {
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
.col-lg {
|
||||
flex: 1 0 0%;
|
||||
flex: 1 0 0;
|
||||
}
|
||||
.row-cols-lg-auto > * {
|
||||
flex: 0 0 auto;
|
||||
@@ -1519,7 +1523,7 @@ progress {
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
.col-xl {
|
||||
flex: 1 0 0%;
|
||||
flex: 1 0 0;
|
||||
}
|
||||
.row-cols-xl-auto > * {
|
||||
flex: 0 0 auto;
|
||||
@@ -1688,7 +1692,7 @@ progress {
|
||||
}
|
||||
@media (min-width: 1400px) {
|
||||
.col-xxl {
|
||||
flex: 1 0 0%;
|
||||
flex: 1 0 0;
|
||||
}
|
||||
.row-cols-xxl-auto > * {
|
||||
flex: 0 0 auto;
|
||||
@@ -2156,10 +2160,6 @@ progress {
|
||||
display: block;
|
||||
padding: 0;
|
||||
}
|
||||
.form-control::-moz-placeholder {
|
||||
color: var(--bs-secondary-color);
|
||||
opacity: 1;
|
||||
}
|
||||
.form-control::placeholder {
|
||||
color: var(--bs-secondary-color);
|
||||
opacity: 1;
|
||||
@@ -2607,9 +2607,11 @@ textarea.form-control-lg {
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 2;
|
||||
max-width: 100%;
|
||||
height: 100%;
|
||||
padding: 1rem 0.75rem;
|
||||
overflow: hidden;
|
||||
color: rgba(var(--bs-body-color-rgb), 0.65);
|
||||
text-align: start;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
@@ -2627,17 +2629,10 @@ textarea.form-control-lg {
|
||||
.form-floating > .form-control-plaintext {
|
||||
padding: 1rem 0.75rem;
|
||||
}
|
||||
.form-floating > .form-control::-moz-placeholder, .form-floating > .form-control-plaintext::-moz-placeholder {
|
||||
color: transparent;
|
||||
}
|
||||
.form-floating > .form-control::placeholder,
|
||||
.form-floating > .form-control-plaintext::placeholder {
|
||||
color: transparent;
|
||||
}
|
||||
.form-floating > .form-control:not(:-moz-placeholder-shown), .form-floating > .form-control-plaintext:not(:-moz-placeholder-shown) {
|
||||
padding-top: 1.625rem;
|
||||
padding-bottom: 0.625rem;
|
||||
}
|
||||
.form-floating > .form-control:focus, .form-floating > .form-control:not(:placeholder-shown),
|
||||
.form-floating > .form-control-plaintext:focus,
|
||||
.form-floating > .form-control-plaintext:not(:placeholder-shown) {
|
||||
@@ -2652,43 +2647,30 @@ textarea.form-control-lg {
|
||||
.form-floating > .form-select {
|
||||
padding-top: 1.625rem;
|
||||
padding-bottom: 0.625rem;
|
||||
}
|
||||
.form-floating > .form-control:not(:-moz-placeholder-shown) ~ label {
|
||||
color: rgba(var(--bs-body-color-rgb), 0.65);
|
||||
transform: scale(0.85) translateY(-0.5rem) translateX(0.15rem);
|
||||
padding-left: 0.75rem;
|
||||
}
|
||||
.form-floating > .form-control:focus ~ label,
|
||||
.form-floating > .form-control:not(:placeholder-shown) ~ label,
|
||||
.form-floating > .form-control-plaintext ~ label,
|
||||
.form-floating > .form-select ~ label {
|
||||
color: rgba(var(--bs-body-color-rgb), 0.65);
|
||||
transform: scale(0.85) translateY(-0.5rem) translateX(0.15rem);
|
||||
}
|
||||
.form-floating > .form-control:not(:-moz-placeholder-shown) ~ label::after {
|
||||
position: absolute;
|
||||
inset: 1rem 0.375rem;
|
||||
z-index: -1;
|
||||
height: 1.5em;
|
||||
content: "";
|
||||
background-color: var(--bs-body-bg);
|
||||
border-radius: var(--bs-border-radius);
|
||||
}
|
||||
.form-floating > .form-control:focus ~ label::after,
|
||||
.form-floating > .form-control:not(:placeholder-shown) ~ label::after,
|
||||
.form-floating > .form-control-plaintext ~ label::after,
|
||||
.form-floating > .form-select ~ label::after {
|
||||
position: absolute;
|
||||
inset: 1rem 0.375rem;
|
||||
z-index: -1;
|
||||
height: 1.5em;
|
||||
content: "";
|
||||
background-color: var(--bs-body-bg);
|
||||
border-radius: var(--bs-border-radius);
|
||||
}
|
||||
.form-floating > .form-control:-webkit-autofill ~ label {
|
||||
color: rgba(var(--bs-body-color-rgb), 0.65);
|
||||
transform: scale(0.85) translateY(-0.5rem) translateX(0.15rem);
|
||||
}
|
||||
.form-floating > textarea:focus ~ label::after,
|
||||
.form-floating > textarea:not(:placeholder-shown) ~ label::after {
|
||||
position: absolute;
|
||||
inset: 1rem 0.375rem;
|
||||
z-index: -1;
|
||||
height: 1.5em;
|
||||
content: "";
|
||||
background-color: var(--bs-body-bg);
|
||||
border-radius: var(--bs-border-radius);
|
||||
}
|
||||
.form-floating > textarea:disabled ~ label::after {
|
||||
background-color: var(--bs-secondary-bg);
|
||||
}
|
||||
.form-floating > .form-control-plaintext ~ label {
|
||||
border-width: var(--bs-border-width) 0;
|
||||
}
|
||||
@@ -2696,10 +2678,6 @@ textarea.form-control-lg {
|
||||
.form-floating > .form-control:disabled ~ label {
|
||||
color: #6c757d;
|
||||
}
|
||||
.form-floating > :disabled ~ label::after,
|
||||
.form-floating > .form-control:disabled ~ label::after {
|
||||
background-color: var(--bs-secondary-bg);
|
||||
}
|
||||
|
||||
.input-group {
|
||||
position: relative;
|
||||
@@ -2782,7 +2760,7 @@ textarea.form-control-lg {
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
.input-group > :not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback) {
|
||||
margin-left: calc(var(--bs-border-width) * -1);
|
||||
margin-left: calc(-1 * var(--bs-border-width));
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
@@ -2824,7 +2802,7 @@ textarea.form-control-lg {
|
||||
.was-validated .form-control:valid, .form-control.is-valid {
|
||||
border-color: var(--bs-form-valid-border-color);
|
||||
padding-right: calc(1.5em + 0.75rem);
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1'/%3e%3c/svg%3e");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right calc(0.375em + 0.1875rem) center;
|
||||
background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);
|
||||
@@ -2843,7 +2821,7 @@ textarea.form-control-lg {
|
||||
border-color: var(--bs-form-valid-border-color);
|
||||
}
|
||||
.was-validated .form-select:valid:not([multiple]):not([size]), .was-validated .form-select:valid:not([multiple])[size="1"], .form-select.is-valid:not([multiple]):not([size]), .form-select.is-valid:not([multiple])[size="1"] {
|
||||
--bs-form-select-bg-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");
|
||||
--bs-form-select-bg-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1'/%3e%3c/svg%3e");
|
||||
padding-right: 4.125rem;
|
||||
background-position: right 0.75rem center, center right 2.25rem;
|
||||
background-size: 16px 12px, calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);
|
||||
@@ -3755,7 +3733,7 @@ textarea.form-control-lg {
|
||||
}
|
||||
.btn-group > :not(.btn-check:first-child) + .btn,
|
||||
.btn-group > .btn-group:not(:first-child) {
|
||||
margin-left: calc(var(--bs-border-width) * -1);
|
||||
margin-left: calc(-1 * var(--bs-border-width));
|
||||
}
|
||||
.btn-group > .btn:not(:last-child):not(.dropdown-toggle),
|
||||
.btn-group > .btn.dropdown-toggle-split:first-child,
|
||||
@@ -3802,14 +3780,15 @@ textarea.form-control-lg {
|
||||
}
|
||||
.btn-group-vertical > .btn:not(:first-child),
|
||||
.btn-group-vertical > .btn-group:not(:first-child) {
|
||||
margin-top: calc(var(--bs-border-width) * -1);
|
||||
margin-top: calc(-1 * var(--bs-border-width));
|
||||
}
|
||||
.btn-group-vertical > .btn:not(:last-child):not(.dropdown-toggle),
|
||||
.btn-group-vertical > .btn-group:not(:last-child) > .btn {
|
||||
border-bottom-right-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
.btn-group-vertical > .btn ~ .btn,
|
||||
.btn-group-vertical > .btn:nth-child(n+3),
|
||||
.btn-group-vertical > :not(.btn-check) + .btn,
|
||||
.btn-group-vertical > .btn-group:not(:first-child) > .btn {
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
@@ -3933,8 +3912,8 @@ textarea.form-control-lg {
|
||||
|
||||
.nav-justified > .nav-link,
|
||||
.nav-justified .nav-item {
|
||||
flex-basis: 0;
|
||||
flex-grow: 1;
|
||||
flex-basis: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@@ -4035,8 +4014,8 @@ textarea.form-control-lg {
|
||||
}
|
||||
|
||||
.navbar-collapse {
|
||||
flex-basis: 100%;
|
||||
flex-grow: 1;
|
||||
flex-basis: 100%;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@@ -4531,7 +4510,7 @@ textarea.form-control-lg {
|
||||
flex-flow: row wrap;
|
||||
}
|
||||
.card-group > .card {
|
||||
flex: 1 0 0%;
|
||||
flex: 1 0 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.card-group > .card + .card {
|
||||
@@ -4542,24 +4521,24 @@ textarea.form-control-lg {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
.card-group > .card:not(:last-child) .card-img-top,
|
||||
.card-group > .card:not(:last-child) .card-header {
|
||||
.card-group > .card:not(:last-child) > .card-img-top,
|
||||
.card-group > .card:not(:last-child) > .card-header {
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
.card-group > .card:not(:last-child) .card-img-bottom,
|
||||
.card-group > .card:not(:last-child) .card-footer {
|
||||
.card-group > .card:not(:last-child) > .card-img-bottom,
|
||||
.card-group > .card:not(:last-child) > .card-footer {
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
.card-group > .card:not(:first-child) {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
.card-group > .card:not(:first-child) .card-img-top,
|
||||
.card-group > .card:not(:first-child) .card-header {
|
||||
.card-group > .card:not(:first-child) > .card-img-top,
|
||||
.card-group > .card:not(:first-child) > .card-header {
|
||||
border-top-left-radius: 0;
|
||||
}
|
||||
.card-group > .card:not(:first-child) .card-img-bottom,
|
||||
.card-group > .card:not(:first-child) .card-footer {
|
||||
.card-group > .card:not(:first-child) > .card-img-bottom,
|
||||
.card-group > .card:not(:first-child) > .card-footer {
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
}
|
||||
@@ -4576,11 +4555,11 @@ textarea.form-control-lg {
|
||||
--bs-accordion-btn-padding-y: 1rem;
|
||||
--bs-accordion-btn-color: var(--bs-body-color);
|
||||
--bs-accordion-btn-bg: var(--bs-accordion-bg);
|
||||
--bs-accordion-btn-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23212529' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath d='M2 5L8 11L14 5'/%3e%3c/svg%3e");
|
||||
--bs-accordion-btn-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23212529' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath d='m2 5 6 6 6-6'/%3e%3c/svg%3e");
|
||||
--bs-accordion-btn-icon-width: 1.25rem;
|
||||
--bs-accordion-btn-icon-transform: rotate(-180deg);
|
||||
--bs-accordion-btn-icon-transition: transform 0.2s ease-in-out;
|
||||
--bs-accordion-btn-active-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23052c65' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath d='M2 5L8 11L14 5'/%3e%3c/svg%3e");
|
||||
--bs-accordion-btn-active-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23052c65' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath d='m2 5 6 6 6-6'/%3e%3c/svg%3e");
|
||||
--bs-accordion-btn-focus-box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
|
||||
--bs-accordion-body-padding-x: 1.25rem;
|
||||
--bs-accordion-body-padding-y: 1rem;
|
||||
@@ -4690,16 +4669,15 @@ textarea.form-control-lg {
|
||||
.accordion-flush > .accordion-item:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
.accordion-flush > .accordion-item > .accordion-header .accordion-button, .accordion-flush > .accordion-item > .accordion-header .accordion-button.collapsed {
|
||||
border-radius: 0;
|
||||
}
|
||||
.accordion-flush > .accordion-item > .accordion-collapse {
|
||||
.accordion-flush > .accordion-item > .accordion-collapse,
|
||||
.accordion-flush > .accordion-item > .accordion-header .accordion-button,
|
||||
.accordion-flush > .accordion-item > .accordion-header .accordion-button.collapsed {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
[data-bs-theme=dark] .accordion-button::after {
|
||||
--bs-accordion-btn-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%236ea8fe'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");
|
||||
--bs-accordion-btn-active-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%236ea8fe'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");
|
||||
--bs-accordion-btn-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%236ea8fe'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708'/%3e%3c/svg%3e");
|
||||
--bs-accordion-btn-active-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%236ea8fe'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708'/%3e%3c/svg%3e");
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
@@ -4803,7 +4781,7 @@ textarea.form-control-lg {
|
||||
}
|
||||
|
||||
.page-item:not(:first-child) .page-link {
|
||||
margin-left: calc(var(--bs-border-width) * -1);
|
||||
margin-left: calc(-1 * var(--bs-border-width));
|
||||
}
|
||||
.page-item:first-child .page-link {
|
||||
border-top-left-radius: var(--bs-pagination-border-radius);
|
||||
@@ -4952,7 +4930,7 @@ textarea.form-control-lg {
|
||||
|
||||
@keyframes progress-bar-stripes {
|
||||
0% {
|
||||
background-position-x: 1rem;
|
||||
background-position-x: var(--bs-progress-height);
|
||||
}
|
||||
}
|
||||
.progress,
|
||||
@@ -5046,22 +5024,6 @@ textarea.form-control-lg {
|
||||
counter-increment: section;
|
||||
}
|
||||
|
||||
.list-group-item-action {
|
||||
width: 100%;
|
||||
color: var(--bs-list-group-action-color);
|
||||
text-align: inherit;
|
||||
}
|
||||
.list-group-item-action:hover, .list-group-item-action:focus {
|
||||
z-index: 1;
|
||||
color: var(--bs-list-group-action-hover-color);
|
||||
text-decoration: none;
|
||||
background-color: var(--bs-list-group-action-hover-bg);
|
||||
}
|
||||
.list-group-item-action:active {
|
||||
color: var(--bs-list-group-action-active-color);
|
||||
background-color: var(--bs-list-group-action-active-bg);
|
||||
}
|
||||
|
||||
.list-group-item {
|
||||
position: relative;
|
||||
display: block;
|
||||
@@ -5098,6 +5060,22 @@ textarea.form-control-lg {
|
||||
border-top-width: var(--bs-list-group-border-width);
|
||||
}
|
||||
|
||||
.list-group-item-action {
|
||||
width: 100%;
|
||||
color: var(--bs-list-group-action-color);
|
||||
text-align: inherit;
|
||||
}
|
||||
.list-group-item-action:not(.active):hover, .list-group-item-action:not(.active):focus {
|
||||
z-index: 1;
|
||||
color: var(--bs-list-group-action-hover-color);
|
||||
text-decoration: none;
|
||||
background-color: var(--bs-list-group-action-hover-bg);
|
||||
}
|
||||
.list-group-item-action:not(.active):active {
|
||||
color: var(--bs-list-group-action-active-color);
|
||||
background-color: var(--bs-list-group-action-active-bg);
|
||||
}
|
||||
|
||||
.list-group-horizontal {
|
||||
flex-direction: row;
|
||||
}
|
||||
@@ -5357,19 +5335,19 @@ textarea.form-control-lg {
|
||||
|
||||
.btn-close {
|
||||
--bs-btn-close-color: #000;
|
||||
--bs-btn-close-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 0 1 1.414 0L8 6.586 14.293.293a1 1 0 1 1 1.414 1.414L9.414 8l6.293 6.293a1 1 0 0 1-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L6.586 8 .293 1.707a1 1 0 0 1 0-1.414z'/%3e%3c/svg%3e");
|
||||
--bs-btn-close-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 0 1 1.414 0L8 6.586 14.293.293a1 1 0 1 1 1.414 1.414L9.414 8l6.293 6.293a1 1 0 0 1-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L6.586 8 .293 1.707a1 1 0 0 1 0-1.414'/%3e%3c/svg%3e");
|
||||
--bs-btn-close-opacity: 0.5;
|
||||
--bs-btn-close-hover-opacity: 0.75;
|
||||
--bs-btn-close-focus-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
|
||||
--bs-btn-close-focus-opacity: 1;
|
||||
--bs-btn-close-disabled-opacity: 0.25;
|
||||
--bs-btn-close-white-filter: invert(1) grayscale(100%) brightness(200%);
|
||||
box-sizing: content-box;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
padding: 0.25em 0.25em;
|
||||
color: var(--bs-btn-close-color);
|
||||
background: transparent var(--bs-btn-close-bg) center/1em auto no-repeat;
|
||||
filter: var(--bs-btn-close-filter);
|
||||
border: 0;
|
||||
border-radius: 0.375rem;
|
||||
opacity: var(--bs-btn-close-opacity);
|
||||
@@ -5393,11 +5371,16 @@ textarea.form-control-lg {
|
||||
}
|
||||
|
||||
.btn-close-white {
|
||||
filter: var(--bs-btn-close-white-filter);
|
||||
--bs-btn-close-filter: invert(1) grayscale(100%) brightness(200%);
|
||||
}
|
||||
|
||||
[data-bs-theme=dark] .btn-close {
|
||||
filter: var(--bs-btn-close-white-filter);
|
||||
:root,
|
||||
[data-bs-theme=light] {
|
||||
--bs-btn-close-filter: ;
|
||||
}
|
||||
|
||||
[data-bs-theme=dark] {
|
||||
--bs-btn-close-filter: invert(1) grayscale(100%) brightness(200%);
|
||||
}
|
||||
|
||||
.toast {
|
||||
@@ -5474,7 +5457,7 @@ textarea.form-control-lg {
|
||||
--bs-modal-width: 500px;
|
||||
--bs-modal-padding: 1rem;
|
||||
--bs-modal-margin: 0.5rem;
|
||||
--bs-modal-color: ;
|
||||
--bs-modal-color: var(--bs-body-color);
|
||||
--bs-modal-bg: var(--bs-body-bg);
|
||||
--bs-modal-border-color: var(--bs-border-color-translucent);
|
||||
--bs-modal-border-width: var(--bs-border-width);
|
||||
@@ -5510,8 +5493,8 @@ textarea.form-control-lg {
|
||||
pointer-events: none;
|
||||
}
|
||||
.modal.fade .modal-dialog {
|
||||
transition: transform 0.3s ease-out;
|
||||
transform: translate(0, -50px);
|
||||
transition: transform 0.3s ease-out;
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.modal.fade .modal-dialog {
|
||||
@@ -5586,7 +5569,10 @@ textarea.form-control-lg {
|
||||
}
|
||||
.modal-header .btn-close {
|
||||
padding: calc(var(--bs-modal-header-padding-y) * 0.5) calc(var(--bs-modal-header-padding-x) * 0.5);
|
||||
margin: calc(-0.5 * var(--bs-modal-header-padding-y)) calc(-0.5 * var(--bs-modal-header-padding-x)) calc(-0.5 * var(--bs-modal-header-padding-y)) auto;
|
||||
margin-top: calc(-0.5 * var(--bs-modal-header-padding-y));
|
||||
margin-right: calc(-0.5 * var(--bs-modal-header-padding-x));
|
||||
margin-bottom: calc(-0.5 * var(--bs-modal-header-padding-y));
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
@@ -6107,6 +6093,7 @@ textarea.form-control-lg {
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
background: none;
|
||||
filter: var(--bs-carousel-control-icon-filter);
|
||||
border: 0;
|
||||
opacity: 0.5;
|
||||
transition: opacity 0.15s ease;
|
||||
@@ -6145,11 +6132,11 @@ textarea.form-control-lg {
|
||||
}
|
||||
|
||||
.carousel-control-prev-icon {
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z'/%3e%3c/svg%3e") /*rtl:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e")*/;
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0'/%3e%3c/svg%3e") /*rtl:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708'/%3e%3c/svg%3e")*/;
|
||||
}
|
||||
|
||||
.carousel-control-next-icon {
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e") /*rtl:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z'/%3e%3c/svg%3e")*/;
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708'/%3e%3c/svg%3e") /*rtl:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0'/%3e%3c/svg%3e")*/;
|
||||
}
|
||||
|
||||
.carousel-indicators {
|
||||
@@ -6175,7 +6162,7 @@ textarea.form-control-lg {
|
||||
margin-left: 3px;
|
||||
text-indent: -999px;
|
||||
cursor: pointer;
|
||||
background-color: #fff;
|
||||
background-color: var(--bs-carousel-indicator-active-bg);
|
||||
background-clip: padding-box;
|
||||
border: 0;
|
||||
border-top: 10px solid transparent;
|
||||
@@ -6199,36 +6186,33 @@ textarea.form-control-lg {
|
||||
left: 15%;
|
||||
padding-top: 1.25rem;
|
||||
padding-bottom: 1.25rem;
|
||||
color: #fff;
|
||||
color: var(--bs-carousel-caption-color);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.carousel-dark .carousel-control-prev-icon,
|
||||
.carousel-dark .carousel-control-next-icon {
|
||||
filter: invert(1) grayscale(100);
|
||||
}
|
||||
.carousel-dark .carousel-indicators [data-bs-target] {
|
||||
background-color: #000;
|
||||
}
|
||||
.carousel-dark .carousel-caption {
|
||||
color: #000;
|
||||
.carousel-dark {
|
||||
--bs-carousel-indicator-active-bg: #000;
|
||||
--bs-carousel-caption-color: #000;
|
||||
--bs-carousel-control-icon-filter: invert(1) grayscale(100);
|
||||
}
|
||||
|
||||
[data-bs-theme=dark] .carousel .carousel-control-prev-icon,
|
||||
[data-bs-theme=dark] .carousel .carousel-control-next-icon, [data-bs-theme=dark].carousel .carousel-control-prev-icon,
|
||||
[data-bs-theme=dark].carousel .carousel-control-next-icon {
|
||||
filter: invert(1) grayscale(100);
|
||||
:root,
|
||||
[data-bs-theme=light] {
|
||||
--bs-carousel-indicator-active-bg: #fff;
|
||||
--bs-carousel-caption-color: #fff;
|
||||
--bs-carousel-control-icon-filter: ;
|
||||
}
|
||||
[data-bs-theme=dark] .carousel .carousel-indicators [data-bs-target], [data-bs-theme=dark].carousel .carousel-indicators [data-bs-target] {
|
||||
background-color: #000;
|
||||
}
|
||||
[data-bs-theme=dark] .carousel .carousel-caption, [data-bs-theme=dark].carousel .carousel-caption {
|
||||
color: #000;
|
||||
|
||||
[data-bs-theme=dark] {
|
||||
--bs-carousel-indicator-active-bg: #000;
|
||||
--bs-carousel-caption-color: #000;
|
||||
--bs-carousel-control-icon-filter: invert(1) grayscale(100);
|
||||
}
|
||||
|
||||
.spinner-grow,
|
||||
.spinner-border {
|
||||
display: inline-block;
|
||||
flex-shrink: 0;
|
||||
width: var(--bs-spinner-width);
|
||||
height: var(--bs-spinner-height);
|
||||
vertical-align: var(--bs-spinner-vertical-align);
|
||||
@@ -6773,7 +6757,10 @@ textarea.form-control-lg {
|
||||
}
|
||||
.offcanvas-header .btn-close {
|
||||
padding: calc(var(--bs-offcanvas-padding-y) * 0.5) calc(var(--bs-offcanvas-padding-x) * 0.5);
|
||||
margin: calc(-0.5 * var(--bs-offcanvas-padding-y)) calc(-0.5 * var(--bs-offcanvas-padding-x)) calc(-0.5 * var(--bs-offcanvas-padding-y)) auto;
|
||||
margin-top: calc(-0.5 * var(--bs-offcanvas-padding-y));
|
||||
margin-right: calc(-0.5 * var(--bs-offcanvas-padding-x));
|
||||
margin-bottom: calc(-0.5 * var(--bs-offcanvas-padding-y));
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.offcanvas-title {
|
||||
@@ -7174,6 +7161,10 @@ textarea.form-control-lg {
|
||||
.visually-hidden-focusable:not(:focus):not(:focus-within):not(caption) {
|
||||
position: absolute !important;
|
||||
}
|
||||
.visually-hidden *,
|
||||
.visually-hidden-focusable:not(:focus):not(:focus-within) * {
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
.stretched-link::after {
|
||||
position: absolute;
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,7 +1,7 @@
|
||||
@charset "UTF-8";
|
||||
/*!
|
||||
* Bootstrap v5.3.3 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2024 The Bootstrap Authors
|
||||
* Bootstrap v5.3.8 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2025 The Bootstrap Authors
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||
*/
|
||||
:root,
|
||||
@@ -517,8 +517,8 @@ legend {
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: calc(1.275rem + 0.3vw);
|
||||
line-height: inherit;
|
||||
font-size: calc(1.275rem + 0.3vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
legend {
|
||||
@@ -547,6 +547,10 @@ legend + * {
|
||||
-webkit-appearance: textfield;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
[type=search]::-webkit-search-cancel-button {
|
||||
cursor: pointer;
|
||||
filter: grayscale(1);
|
||||
}
|
||||
|
||||
[type="tel"],
|
||||
[type="url"],
|
||||
@@ -599,9 +603,9 @@ progress {
|
||||
}
|
||||
|
||||
.display-1 {
|
||||
font-size: calc(1.625rem + 4.5vw);
|
||||
font-weight: 300;
|
||||
line-height: 1.2;
|
||||
font-size: calc(1.625rem + 4.5vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
.display-1 {
|
||||
@@ -610,9 +614,9 @@ progress {
|
||||
}
|
||||
|
||||
.display-2 {
|
||||
font-size: calc(1.575rem + 3.9vw);
|
||||
font-weight: 300;
|
||||
line-height: 1.2;
|
||||
font-size: calc(1.575rem + 3.9vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
.display-2 {
|
||||
@@ -621,9 +625,9 @@ progress {
|
||||
}
|
||||
|
||||
.display-3 {
|
||||
font-size: calc(1.525rem + 3.3vw);
|
||||
font-weight: 300;
|
||||
line-height: 1.2;
|
||||
font-size: calc(1.525rem + 3.3vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
.display-3 {
|
||||
@@ -632,9 +636,9 @@ progress {
|
||||
}
|
||||
|
||||
.display-4 {
|
||||
font-size: calc(1.475rem + 2.7vw);
|
||||
font-weight: 300;
|
||||
line-height: 1.2;
|
||||
font-size: calc(1.475rem + 2.7vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
.display-4 {
|
||||
@@ -643,9 +647,9 @@ progress {
|
||||
}
|
||||
|
||||
.display-5 {
|
||||
font-size: calc(1.425rem + 2.1vw);
|
||||
font-weight: 300;
|
||||
line-height: 1.2;
|
||||
font-size: calc(1.425rem + 2.1vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
.display-5 {
|
||||
@@ -654,9 +658,9 @@ progress {
|
||||
}
|
||||
|
||||
.display-6 {
|
||||
font-size: calc(1.375rem + 1.5vw);
|
||||
font-weight: 300;
|
||||
line-height: 1.2;
|
||||
font-size: calc(1.375rem + 1.5vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
.display-6 {
|
||||
@@ -801,7 +805,7 @@ progress {
|
||||
}
|
||||
|
||||
.col {
|
||||
flex: 1 0 0%;
|
||||
flex: 1 0 0;
|
||||
}
|
||||
|
||||
.row-cols-auto > * {
|
||||
@@ -1010,7 +1014,7 @@ progress {
|
||||
|
||||
@media (min-width: 576px) {
|
||||
.col-sm {
|
||||
flex: 1 0 0%;
|
||||
flex: 1 0 0;
|
||||
}
|
||||
.row-cols-sm-auto > * {
|
||||
flex: 0 0 auto;
|
||||
@@ -1179,7 +1183,7 @@ progress {
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
.col-md {
|
||||
flex: 1 0 0%;
|
||||
flex: 1 0 0;
|
||||
}
|
||||
.row-cols-md-auto > * {
|
||||
flex: 0 0 auto;
|
||||
@@ -1348,7 +1352,7 @@ progress {
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
.col-lg {
|
||||
flex: 1 0 0%;
|
||||
flex: 1 0 0;
|
||||
}
|
||||
.row-cols-lg-auto > * {
|
||||
flex: 0 0 auto;
|
||||
@@ -1517,7 +1521,7 @@ progress {
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
.col-xl {
|
||||
flex: 1 0 0%;
|
||||
flex: 1 0 0;
|
||||
}
|
||||
.row-cols-xl-auto > * {
|
||||
flex: 0 0 auto;
|
||||
@@ -1686,7 +1690,7 @@ progress {
|
||||
}
|
||||
@media (min-width: 1400px) {
|
||||
.col-xxl {
|
||||
flex: 1 0 0%;
|
||||
flex: 1 0 0;
|
||||
}
|
||||
.row-cols-xxl-auto > * {
|
||||
flex: 0 0 auto;
|
||||
@@ -2154,10 +2158,6 @@ progress {
|
||||
display: block;
|
||||
padding: 0;
|
||||
}
|
||||
.form-control::-moz-placeholder {
|
||||
color: var(--bs-secondary-color);
|
||||
opacity: 1;
|
||||
}
|
||||
.form-control::placeholder {
|
||||
color: var(--bs-secondary-color);
|
||||
opacity: 1;
|
||||
@@ -2605,9 +2605,11 @@ textarea.form-control-lg {
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: 2;
|
||||
max-width: 100%;
|
||||
height: 100%;
|
||||
padding: 1rem 0.75rem;
|
||||
overflow: hidden;
|
||||
color: rgba(var(--bs-body-color-rgb), 0.65);
|
||||
text-align: start;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
@@ -2625,17 +2627,10 @@ textarea.form-control-lg {
|
||||
.form-floating > .form-control-plaintext {
|
||||
padding: 1rem 0.75rem;
|
||||
}
|
||||
.form-floating > .form-control::-moz-placeholder, .form-floating > .form-control-plaintext::-moz-placeholder {
|
||||
color: transparent;
|
||||
}
|
||||
.form-floating > .form-control::placeholder,
|
||||
.form-floating > .form-control-plaintext::placeholder {
|
||||
color: transparent;
|
||||
}
|
||||
.form-floating > .form-control:not(:-moz-placeholder-shown), .form-floating > .form-control-plaintext:not(:-moz-placeholder-shown) {
|
||||
padding-top: 1.625rem;
|
||||
padding-bottom: 0.625rem;
|
||||
}
|
||||
.form-floating > .form-control:focus, .form-floating > .form-control:not(:placeholder-shown),
|
||||
.form-floating > .form-control-plaintext:focus,
|
||||
.form-floating > .form-control-plaintext:not(:placeholder-shown) {
|
||||
@@ -2650,43 +2645,30 @@ textarea.form-control-lg {
|
||||
.form-floating > .form-select {
|
||||
padding-top: 1.625rem;
|
||||
padding-bottom: 0.625rem;
|
||||
}
|
||||
.form-floating > .form-control:not(:-moz-placeholder-shown) ~ label {
|
||||
color: rgba(var(--bs-body-color-rgb), 0.65);
|
||||
transform: scale(0.85) translateY(-0.5rem) translateX(-0.15rem);
|
||||
padding-right: 0.75rem;
|
||||
}
|
||||
.form-floating > .form-control:focus ~ label,
|
||||
.form-floating > .form-control:not(:placeholder-shown) ~ label,
|
||||
.form-floating > .form-control-plaintext ~ label,
|
||||
.form-floating > .form-select ~ label {
|
||||
color: rgba(var(--bs-body-color-rgb), 0.65);
|
||||
transform: scale(0.85) translateY(-0.5rem) translateX(-0.15rem);
|
||||
}
|
||||
.form-floating > .form-control:not(:-moz-placeholder-shown) ~ label::after {
|
||||
position: absolute;
|
||||
inset: 1rem 0.375rem;
|
||||
z-index: -1;
|
||||
height: 1.5em;
|
||||
content: "";
|
||||
background-color: var(--bs-body-bg);
|
||||
border-radius: var(--bs-border-radius);
|
||||
}
|
||||
.form-floating > .form-control:focus ~ label::after,
|
||||
.form-floating > .form-control:not(:placeholder-shown) ~ label::after,
|
||||
.form-floating > .form-control-plaintext ~ label::after,
|
||||
.form-floating > .form-select ~ label::after {
|
||||
position: absolute;
|
||||
inset: 1rem 0.375rem;
|
||||
z-index: -1;
|
||||
height: 1.5em;
|
||||
content: "";
|
||||
background-color: var(--bs-body-bg);
|
||||
border-radius: var(--bs-border-radius);
|
||||
}
|
||||
.form-floating > .form-control:-webkit-autofill ~ label {
|
||||
color: rgba(var(--bs-body-color-rgb), 0.65);
|
||||
transform: scale(0.85) translateY(-0.5rem) translateX(-0.15rem);
|
||||
}
|
||||
.form-floating > textarea:focus ~ label::after,
|
||||
.form-floating > textarea:not(:placeholder-shown) ~ label::after {
|
||||
position: absolute;
|
||||
inset: 1rem 0.375rem;
|
||||
z-index: -1;
|
||||
height: 1.5em;
|
||||
content: "";
|
||||
background-color: var(--bs-body-bg);
|
||||
border-radius: var(--bs-border-radius);
|
||||
}
|
||||
.form-floating > textarea:disabled ~ label::after {
|
||||
background-color: var(--bs-secondary-bg);
|
||||
}
|
||||
.form-floating > .form-control-plaintext ~ label {
|
||||
border-width: var(--bs-border-width) 0;
|
||||
}
|
||||
@@ -2694,10 +2676,6 @@ textarea.form-control-lg {
|
||||
.form-floating > .form-control:disabled ~ label {
|
||||
color: #6c757d;
|
||||
}
|
||||
.form-floating > :disabled ~ label::after,
|
||||
.form-floating > .form-control:disabled ~ label::after {
|
||||
background-color: var(--bs-secondary-bg);
|
||||
}
|
||||
|
||||
.input-group {
|
||||
position: relative;
|
||||
@@ -2780,7 +2758,7 @@ textarea.form-control-lg {
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
.input-group > :not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback) {
|
||||
margin-right: calc(var(--bs-border-width) * -1);
|
||||
margin-right: calc(-1 * var(--bs-border-width));
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
@@ -2822,7 +2800,7 @@ textarea.form-control-lg {
|
||||
.was-validated .form-control:valid, .form-control.is-valid {
|
||||
border-color: var(--bs-form-valid-border-color);
|
||||
padding-left: calc(1.5em + 0.75rem);
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1'/%3e%3c/svg%3e");
|
||||
background-repeat: no-repeat;
|
||||
background-position: left calc(0.375em + 0.1875rem) center;
|
||||
background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);
|
||||
@@ -2841,7 +2819,7 @@ textarea.form-control-lg {
|
||||
border-color: var(--bs-form-valid-border-color);
|
||||
}
|
||||
.was-validated .form-select:valid:not([multiple]):not([size]), .was-validated .form-select:valid:not([multiple])[size="1"], .form-select.is-valid:not([multiple]):not([size]), .form-select.is-valid:not([multiple])[size="1"] {
|
||||
--bs-form-select-bg-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");
|
||||
--bs-form-select-bg-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1'/%3e%3c/svg%3e");
|
||||
padding-left: 4.125rem;
|
||||
background-position: left 0.75rem center, center left 2.25rem;
|
||||
background-size: 16px 12px, calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);
|
||||
@@ -2973,6 +2951,7 @@ textarea.form-control-lg {
|
||||
.btn {
|
||||
--bs-btn-padding-x: 0.75rem;
|
||||
--bs-btn-padding-y: 0.375rem;
|
||||
--bs-btn-font-family: ;
|
||||
--bs-btn-font-size: 1rem;
|
||||
--bs-btn-font-weight: 400;
|
||||
--bs-btn-line-height: 1.5;
|
||||
@@ -3701,6 +3680,7 @@ textarea.form-control-lg {
|
||||
--bs-dropdown-color: #dee2e6;
|
||||
--bs-dropdown-bg: #343a40;
|
||||
--bs-dropdown-border-color: var(--bs-border-color-translucent);
|
||||
--bs-dropdown-box-shadow: ;
|
||||
--bs-dropdown-link-color: #dee2e6;
|
||||
--bs-dropdown-link-hover-color: #fff;
|
||||
--bs-dropdown-divider-bg: var(--bs-border-color-translucent);
|
||||
@@ -3751,7 +3731,7 @@ textarea.form-control-lg {
|
||||
}
|
||||
.btn-group > :not(.btn-check:first-child) + .btn,
|
||||
.btn-group > .btn-group:not(:first-child) {
|
||||
margin-right: calc(var(--bs-border-width) * -1);
|
||||
margin-right: calc(-1 * var(--bs-border-width));
|
||||
}
|
||||
.btn-group > .btn:not(:last-child):not(.dropdown-toggle),
|
||||
.btn-group > .btn.dropdown-toggle-split:first-child,
|
||||
@@ -3798,14 +3778,15 @@ textarea.form-control-lg {
|
||||
}
|
||||
.btn-group-vertical > .btn:not(:first-child),
|
||||
.btn-group-vertical > .btn-group:not(:first-child) {
|
||||
margin-top: calc(var(--bs-border-width) * -1);
|
||||
margin-top: calc(-1 * var(--bs-border-width));
|
||||
}
|
||||
.btn-group-vertical > .btn:not(:last-child):not(.dropdown-toggle),
|
||||
.btn-group-vertical > .btn-group:not(:last-child) > .btn {
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
.btn-group-vertical > .btn ~ .btn,
|
||||
.btn-group-vertical > .btn:nth-child(n+3),
|
||||
.btn-group-vertical > :not(.btn-check) + .btn,
|
||||
.btn-group-vertical > .btn-group:not(:first-child) > .btn {
|
||||
border-top-right-radius: 0;
|
||||
border-top-left-radius: 0;
|
||||
@@ -3814,6 +3795,7 @@ textarea.form-control-lg {
|
||||
.nav {
|
||||
--bs-nav-link-padding-x: 1rem;
|
||||
--bs-nav-link-padding-y: 0.5rem;
|
||||
--bs-nav-link-font-weight: ;
|
||||
--bs-nav-link-color: var(--bs-link-color);
|
||||
--bs-nav-link-hover-color: var(--bs-link-hover-color);
|
||||
--bs-nav-link-disabled-color: var(--bs-secondary-color);
|
||||
@@ -3928,8 +3910,8 @@ textarea.form-control-lg {
|
||||
|
||||
.nav-justified > .nav-link,
|
||||
.nav-justified .nav-item {
|
||||
flex-basis: 0;
|
||||
flex-grow: 1;
|
||||
flex-basis: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@@ -4001,6 +3983,7 @@ textarea.form-control-lg {
|
||||
.navbar-nav {
|
||||
--bs-nav-link-padding-x: 0;
|
||||
--bs-nav-link-padding-y: 0.5rem;
|
||||
--bs-nav-link-font-weight: ;
|
||||
--bs-nav-link-color: var(--bs-navbar-color);
|
||||
--bs-nav-link-hover-color: var(--bs-navbar-hover-color);
|
||||
--bs-nav-link-disabled-color: var(--bs-navbar-disabled-color);
|
||||
@@ -4029,8 +4012,8 @@ textarea.form-control-lg {
|
||||
}
|
||||
|
||||
.navbar-collapse {
|
||||
flex-basis: 100%;
|
||||
flex-grow: 1;
|
||||
flex-basis: 100%;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@@ -4375,13 +4358,19 @@ textarea.form-control-lg {
|
||||
--bs-card-spacer-y: 1rem;
|
||||
--bs-card-spacer-x: 1rem;
|
||||
--bs-card-title-spacer-y: 0.5rem;
|
||||
--bs-card-title-color: ;
|
||||
--bs-card-subtitle-color: ;
|
||||
--bs-card-border-width: var(--bs-border-width);
|
||||
--bs-card-border-color: var(--bs-border-color-translucent);
|
||||
--bs-card-border-radius: var(--bs-border-radius);
|
||||
--bs-card-box-shadow: ;
|
||||
--bs-card-inner-border-radius: calc(var(--bs-border-radius) - (var(--bs-border-width)));
|
||||
--bs-card-cap-padding-y: 0.5rem;
|
||||
--bs-card-cap-padding-x: 1rem;
|
||||
--bs-card-cap-bg: rgba(var(--bs-body-color-rgb), 0.03);
|
||||
--bs-card-cap-color: ;
|
||||
--bs-card-height: ;
|
||||
--bs-card-color: ;
|
||||
--bs-card-bg: var(--bs-body-bg);
|
||||
--bs-card-img-overlay-padding: 1rem;
|
||||
--bs-card-group-margin: 0.75rem;
|
||||
@@ -4519,7 +4508,7 @@ textarea.form-control-lg {
|
||||
flex-flow: row wrap;
|
||||
}
|
||||
.card-group > .card {
|
||||
flex: 1 0 0%;
|
||||
flex: 1 0 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.card-group > .card + .card {
|
||||
@@ -4530,24 +4519,24 @@ textarea.form-control-lg {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
.card-group > .card:not(:last-child) .card-img-top,
|
||||
.card-group > .card:not(:last-child) .card-header {
|
||||
.card-group > .card:not(:last-child) > .card-img-top,
|
||||
.card-group > .card:not(:last-child) > .card-header {
|
||||
border-top-left-radius: 0;
|
||||
}
|
||||
.card-group > .card:not(:last-child) .card-img-bottom,
|
||||
.card-group > .card:not(:last-child) .card-footer {
|
||||
.card-group > .card:not(:last-child) > .card-img-bottom,
|
||||
.card-group > .card:not(:last-child) > .card-footer {
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
.card-group > .card:not(:first-child) {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
.card-group > .card:not(:first-child) .card-img-top,
|
||||
.card-group > .card:not(:first-child) .card-header {
|
||||
.card-group > .card:not(:first-child) > .card-img-top,
|
||||
.card-group > .card:not(:first-child) > .card-header {
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
.card-group > .card:not(:first-child) .card-img-bottom,
|
||||
.card-group > .card:not(:first-child) .card-footer {
|
||||
.card-group > .card:not(:first-child) > .card-img-bottom,
|
||||
.card-group > .card:not(:first-child) > .card-footer {
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
}
|
||||
@@ -4564,11 +4553,11 @@ textarea.form-control-lg {
|
||||
--bs-accordion-btn-padding-y: 1rem;
|
||||
--bs-accordion-btn-color: var(--bs-body-color);
|
||||
--bs-accordion-btn-bg: var(--bs-accordion-bg);
|
||||
--bs-accordion-btn-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23212529' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath d='M2 5L8 11L14 5'/%3e%3c/svg%3e");
|
||||
--bs-accordion-btn-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23212529' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath d='m2 5 6 6 6-6'/%3e%3c/svg%3e");
|
||||
--bs-accordion-btn-icon-width: 1.25rem;
|
||||
--bs-accordion-btn-icon-transform: rotate(-180deg);
|
||||
--bs-accordion-btn-icon-transition: transform 0.2s ease-in-out;
|
||||
--bs-accordion-btn-active-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23052c65' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath d='M2 5L8 11L14 5'/%3e%3c/svg%3e");
|
||||
--bs-accordion-btn-active-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23052c65' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath d='m2 5 6 6 6-6'/%3e%3c/svg%3e");
|
||||
--bs-accordion-btn-focus-box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
|
||||
--bs-accordion-body-padding-x: 1.25rem;
|
||||
--bs-accordion-body-padding-y: 1rem;
|
||||
@@ -4678,22 +4667,23 @@ textarea.form-control-lg {
|
||||
.accordion-flush > .accordion-item:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
.accordion-flush > .accordion-item > .accordion-header .accordion-button, .accordion-flush > .accordion-item > .accordion-header .accordion-button.collapsed {
|
||||
border-radius: 0;
|
||||
}
|
||||
.accordion-flush > .accordion-item > .accordion-collapse {
|
||||
.accordion-flush > .accordion-item > .accordion-collapse,
|
||||
.accordion-flush > .accordion-item > .accordion-header .accordion-button,
|
||||
.accordion-flush > .accordion-item > .accordion-header .accordion-button.collapsed {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
[data-bs-theme=dark] .accordion-button::after {
|
||||
--bs-accordion-btn-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%236ea8fe'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");
|
||||
--bs-accordion-btn-active-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%236ea8fe'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");
|
||||
--bs-accordion-btn-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%236ea8fe'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708'/%3e%3c/svg%3e");
|
||||
--bs-accordion-btn-active-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%236ea8fe'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708'/%3e%3c/svg%3e");
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
--bs-breadcrumb-padding-x: 0;
|
||||
--bs-breadcrumb-padding-y: 0;
|
||||
--bs-breadcrumb-margin-bottom: 1rem;
|
||||
--bs-breadcrumb-bg: ;
|
||||
--bs-breadcrumb-border-radius: ;
|
||||
--bs-breadcrumb-divider-color: var(--bs-secondary-color);
|
||||
--bs-breadcrumb-item-padding-x: 0.5rem;
|
||||
--bs-breadcrumb-item-active-color: var(--bs-secondary-color);
|
||||
@@ -4789,7 +4779,7 @@ textarea.form-control-lg {
|
||||
}
|
||||
|
||||
.page-item:not(:first-child) .page-link {
|
||||
margin-right: calc(var(--bs-border-width) * -1);
|
||||
margin-right: calc(-1 * var(--bs-border-width));
|
||||
}
|
||||
.page-item:first-child .page-link {
|
||||
border-top-right-radius: var(--bs-pagination-border-radius);
|
||||
@@ -4938,7 +4928,7 @@ textarea.form-control-lg {
|
||||
|
||||
@keyframes progress-bar-stripes {
|
||||
0% {
|
||||
background-position-x: 1rem;
|
||||
background-position-x: var(--bs-progress-height);
|
||||
}
|
||||
}
|
||||
.progress,
|
||||
@@ -5032,22 +5022,6 @@ textarea.form-control-lg {
|
||||
counter-increment: section;
|
||||
}
|
||||
|
||||
.list-group-item-action {
|
||||
width: 100%;
|
||||
color: var(--bs-list-group-action-color);
|
||||
text-align: inherit;
|
||||
}
|
||||
.list-group-item-action:hover, .list-group-item-action:focus {
|
||||
z-index: 1;
|
||||
color: var(--bs-list-group-action-hover-color);
|
||||
text-decoration: none;
|
||||
background-color: var(--bs-list-group-action-hover-bg);
|
||||
}
|
||||
.list-group-item-action:active {
|
||||
color: var(--bs-list-group-action-active-color);
|
||||
background-color: var(--bs-list-group-action-active-bg);
|
||||
}
|
||||
|
||||
.list-group-item {
|
||||
position: relative;
|
||||
display: block;
|
||||
@@ -5084,6 +5058,22 @@ textarea.form-control-lg {
|
||||
border-top-width: var(--bs-list-group-border-width);
|
||||
}
|
||||
|
||||
.list-group-item-action {
|
||||
width: 100%;
|
||||
color: var(--bs-list-group-action-color);
|
||||
text-align: inherit;
|
||||
}
|
||||
.list-group-item-action:not(.active):hover, .list-group-item-action:not(.active):focus {
|
||||
z-index: 1;
|
||||
color: var(--bs-list-group-action-hover-color);
|
||||
text-decoration: none;
|
||||
background-color: var(--bs-list-group-action-hover-bg);
|
||||
}
|
||||
.list-group-item-action:not(.active):active {
|
||||
color: var(--bs-list-group-action-active-color);
|
||||
background-color: var(--bs-list-group-action-active-bg);
|
||||
}
|
||||
|
||||
.list-group-horizontal {
|
||||
flex-direction: row;
|
||||
}
|
||||
@@ -5343,19 +5333,19 @@ textarea.form-control-lg {
|
||||
|
||||
.btn-close {
|
||||
--bs-btn-close-color: #000;
|
||||
--bs-btn-close-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 0 1 1.414 0L8 6.586 14.293.293a1 1 0 1 1 1.414 1.414L9.414 8l6.293 6.293a1 1 0 0 1-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L6.586 8 .293 1.707a1 1 0 0 1 0-1.414z'/%3e%3c/svg%3e");
|
||||
--bs-btn-close-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 0 1 1.414 0L8 6.586 14.293.293a1 1 0 1 1 1.414 1.414L9.414 8l6.293 6.293a1 1 0 0 1-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L6.586 8 .293 1.707a1 1 0 0 1 0-1.414'/%3e%3c/svg%3e");
|
||||
--bs-btn-close-opacity: 0.5;
|
||||
--bs-btn-close-hover-opacity: 0.75;
|
||||
--bs-btn-close-focus-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
|
||||
--bs-btn-close-focus-opacity: 1;
|
||||
--bs-btn-close-disabled-opacity: 0.25;
|
||||
--bs-btn-close-white-filter: invert(1) grayscale(100%) brightness(200%);
|
||||
box-sizing: content-box;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
padding: 0.25em 0.25em;
|
||||
color: var(--bs-btn-close-color);
|
||||
background: transparent var(--bs-btn-close-bg) center/1em auto no-repeat;
|
||||
filter: var(--bs-btn-close-filter);
|
||||
border: 0;
|
||||
border-radius: 0.375rem;
|
||||
opacity: var(--bs-btn-close-opacity);
|
||||
@@ -5379,11 +5369,16 @@ textarea.form-control-lg {
|
||||
}
|
||||
|
||||
.btn-close-white {
|
||||
filter: var(--bs-btn-close-white-filter);
|
||||
--bs-btn-close-filter: invert(1) grayscale(100%) brightness(200%);
|
||||
}
|
||||
|
||||
[data-bs-theme=dark] .btn-close {
|
||||
filter: var(--bs-btn-close-white-filter);
|
||||
:root,
|
||||
[data-bs-theme=light] {
|
||||
--bs-btn-close-filter: ;
|
||||
}
|
||||
|
||||
[data-bs-theme=dark] {
|
||||
--bs-btn-close-filter: invert(1) grayscale(100%) brightness(200%);
|
||||
}
|
||||
|
||||
.toast {
|
||||
@@ -5393,6 +5388,7 @@ textarea.form-control-lg {
|
||||
--bs-toast-spacing: 1.5rem;
|
||||
--bs-toast-max-width: 350px;
|
||||
--bs-toast-font-size: 0.875rem;
|
||||
--bs-toast-color: ;
|
||||
--bs-toast-bg: rgba(var(--bs-body-bg-rgb), 0.85);
|
||||
--bs-toast-border-width: var(--bs-border-width);
|
||||
--bs-toast-border-color: var(--bs-border-color-translucent);
|
||||
@@ -5459,6 +5455,7 @@ textarea.form-control-lg {
|
||||
--bs-modal-width: 500px;
|
||||
--bs-modal-padding: 1rem;
|
||||
--bs-modal-margin: 0.5rem;
|
||||
--bs-modal-color: var(--bs-body-color);
|
||||
--bs-modal-bg: var(--bs-body-bg);
|
||||
--bs-modal-border-color: var(--bs-border-color-translucent);
|
||||
--bs-modal-border-width: var(--bs-border-width);
|
||||
@@ -5472,6 +5469,7 @@ textarea.form-control-lg {
|
||||
--bs-modal-header-border-width: var(--bs-border-width);
|
||||
--bs-modal-title-line-height: 1.5;
|
||||
--bs-modal-footer-gap: 0.5rem;
|
||||
--bs-modal-footer-bg: ;
|
||||
--bs-modal-footer-border-color: var(--bs-border-color);
|
||||
--bs-modal-footer-border-width: var(--bs-border-width);
|
||||
position: fixed;
|
||||
@@ -5493,8 +5491,8 @@ textarea.form-control-lg {
|
||||
pointer-events: none;
|
||||
}
|
||||
.modal.fade .modal-dialog {
|
||||
transition: transform 0.3s ease-out;
|
||||
transform: translate(0, -50px);
|
||||
transition: transform 0.3s ease-out;
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.modal.fade .modal-dialog {
|
||||
@@ -5569,7 +5567,10 @@ textarea.form-control-lg {
|
||||
}
|
||||
.modal-header .btn-close {
|
||||
padding: calc(var(--bs-modal-header-padding-y) * 0.5) calc(var(--bs-modal-header-padding-x) * 0.5);
|
||||
margin: calc(-0.5 * var(--bs-modal-header-padding-y)) auto calc(-0.5 * var(--bs-modal-header-padding-y)) calc(-0.5 * var(--bs-modal-header-padding-x));
|
||||
margin-top: calc(-0.5 * var(--bs-modal-header-padding-y));
|
||||
margin-left: calc(-0.5 * var(--bs-modal-header-padding-x));
|
||||
margin-bottom: calc(-0.5 * var(--bs-modal-header-padding-y));
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
@@ -5748,6 +5749,7 @@ textarea.form-control-lg {
|
||||
--bs-tooltip-max-width: 200px;
|
||||
--bs-tooltip-padding-x: 0.5rem;
|
||||
--bs-tooltip-padding-y: 0.25rem;
|
||||
--bs-tooltip-margin: ;
|
||||
--bs-tooltip-font-size: 0.875rem;
|
||||
--bs-tooltip-color: var(--bs-body-bg);
|
||||
--bs-tooltip-bg: var(--bs-emphasis-color);
|
||||
@@ -6073,6 +6075,7 @@ textarea.form-control-lg {
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
background: none;
|
||||
filter: var(--bs-carousel-control-icon-filter);
|
||||
border: 0;
|
||||
opacity: 0.5;
|
||||
transition: opacity 0.15s ease;
|
||||
@@ -6111,11 +6114,11 @@ textarea.form-control-lg {
|
||||
}
|
||||
|
||||
.carousel-control-prev-icon {
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708'/%3e%3c/svg%3e");
|
||||
}
|
||||
|
||||
.carousel-control-next-icon {
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z'/%3e%3c/svg%3e");
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0'/%3e%3c/svg%3e");
|
||||
}
|
||||
|
||||
.carousel-indicators {
|
||||
@@ -6141,7 +6144,7 @@ textarea.form-control-lg {
|
||||
margin-right: 3px;
|
||||
text-indent: -999px;
|
||||
cursor: pointer;
|
||||
background-color: #fff;
|
||||
background-color: var(--bs-carousel-indicator-active-bg);
|
||||
background-clip: padding-box;
|
||||
border: 0;
|
||||
border-top: 10px solid transparent;
|
||||
@@ -6165,36 +6168,33 @@ textarea.form-control-lg {
|
||||
right: 15%;
|
||||
padding-top: 1.25rem;
|
||||
padding-bottom: 1.25rem;
|
||||
color: #fff;
|
||||
color: var(--bs-carousel-caption-color);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.carousel-dark .carousel-control-prev-icon,
|
||||
.carousel-dark .carousel-control-next-icon {
|
||||
filter: invert(1) grayscale(100);
|
||||
}
|
||||
.carousel-dark .carousel-indicators [data-bs-target] {
|
||||
background-color: #000;
|
||||
}
|
||||
.carousel-dark .carousel-caption {
|
||||
color: #000;
|
||||
.carousel-dark {
|
||||
--bs-carousel-indicator-active-bg: #000;
|
||||
--bs-carousel-caption-color: #000;
|
||||
--bs-carousel-control-icon-filter: invert(1) grayscale(100);
|
||||
}
|
||||
|
||||
[data-bs-theme=dark] .carousel .carousel-control-prev-icon,
|
||||
[data-bs-theme=dark] .carousel .carousel-control-next-icon, [data-bs-theme=dark].carousel .carousel-control-prev-icon,
|
||||
[data-bs-theme=dark].carousel .carousel-control-next-icon {
|
||||
filter: invert(1) grayscale(100);
|
||||
:root,
|
||||
[data-bs-theme=light] {
|
||||
--bs-carousel-indicator-active-bg: #fff;
|
||||
--bs-carousel-caption-color: #fff;
|
||||
--bs-carousel-control-icon-filter: ;
|
||||
}
|
||||
[data-bs-theme=dark] .carousel .carousel-indicators [data-bs-target], [data-bs-theme=dark].carousel .carousel-indicators [data-bs-target] {
|
||||
background-color: #000;
|
||||
}
|
||||
[data-bs-theme=dark] .carousel .carousel-caption, [data-bs-theme=dark].carousel .carousel-caption {
|
||||
color: #000;
|
||||
|
||||
[data-bs-theme=dark] {
|
||||
--bs-carousel-indicator-active-bg: #000;
|
||||
--bs-carousel-caption-color: #000;
|
||||
--bs-carousel-control-icon-filter: invert(1) grayscale(100);
|
||||
}
|
||||
|
||||
.spinner-grow,
|
||||
.spinner-border {
|
||||
display: inline-block;
|
||||
flex-shrink: 0;
|
||||
width: var(--bs-spinner-width);
|
||||
height: var(--bs-spinner-height);
|
||||
vertical-align: var(--bs-spinner-vertical-align);
|
||||
@@ -6739,7 +6739,10 @@ textarea.form-control-lg {
|
||||
}
|
||||
.offcanvas-header .btn-close {
|
||||
padding: calc(var(--bs-offcanvas-padding-y) * 0.5) calc(var(--bs-offcanvas-padding-x) * 0.5);
|
||||
margin: calc(-0.5 * var(--bs-offcanvas-padding-y)) auto calc(-0.5 * var(--bs-offcanvas-padding-y)) calc(-0.5 * var(--bs-offcanvas-padding-x));
|
||||
margin-top: calc(-0.5 * var(--bs-offcanvas-padding-y));
|
||||
margin-left: calc(-0.5 * var(--bs-offcanvas-padding-x));
|
||||
margin-bottom: calc(-0.5 * var(--bs-offcanvas-padding-y));
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.offcanvas-title {
|
||||
@@ -7140,6 +7143,10 @@ textarea.form-control-lg {
|
||||
.visually-hidden-focusable:not(:focus):not(:focus-within):not(caption) {
|
||||
position: absolute !important;
|
||||
}
|
||||
.visually-hidden *,
|
||||
.visually-hidden-focusable:not(:focus):not(:focus-within) * {
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
.stretched-link::after {
|
||||
position: absolute;
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,6 +1,6 @@
|
||||
/*!
|
||||
* Bootstrap v5.3.3 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2024 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)
|
||||
* Bootstrap v5.3.8 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2025 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||
*/
|
||||
(function (global, factory) {
|
||||
@@ -205,7 +205,7 @@
|
||||
* @param {HTMLElement} element
|
||||
* @return void
|
||||
*
|
||||
* @see https://www.charistheo.io/blog/2021/02/restart-a-css-animation-with-javascript/#restarting-a-css-animation
|
||||
* @see https://www.harrytheo.com/blog/2021/02/restart-a-css-animation-with-javascript/#restarting-a-css-animation
|
||||
*/
|
||||
const reflow = element => {
|
||||
element.offsetHeight; // eslint-disable-line no-unused-expressions
|
||||
@@ -250,7 +250,7 @@
|
||||
});
|
||||
};
|
||||
const execute = (possibleCallback, args = [], defaultValue = possibleCallback) => {
|
||||
return typeof possibleCallback === 'function' ? possibleCallback(...args) : defaultValue;
|
||||
return typeof possibleCallback === 'function' ? possibleCallback.call(...args) : defaultValue;
|
||||
};
|
||||
const executeAfterTransition = (callback, transitionElement, waitForTransition = true) => {
|
||||
if (!waitForTransition) {
|
||||
@@ -572,7 +572,7 @@
|
||||
const bsKeys = Object.keys(element.dataset).filter(key => key.startsWith('bs') && !key.startsWith('bsConfig'));
|
||||
for (const key of bsKeys) {
|
||||
let pureKey = key.replace(/^bs/, '');
|
||||
pureKey = pureKey.charAt(0).toLowerCase() + pureKey.slice(1, pureKey.length);
|
||||
pureKey = pureKey.charAt(0).toLowerCase() + pureKey.slice(1);
|
||||
attributes[pureKey] = normalizeData(element.dataset[key]);
|
||||
}
|
||||
return attributes;
|
||||
@@ -647,7 +647,7 @@
|
||||
* Constants
|
||||
*/
|
||||
|
||||
const VERSION = '5.3.3';
|
||||
const VERSION = '5.3.8';
|
||||
|
||||
/**
|
||||
* Class definition
|
||||
@@ -673,6 +673,8 @@
|
||||
this[propertyName] = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Private
|
||||
_queueCallback(callback, element, isAnimated = true) {
|
||||
executeAfterTransition(callback, element, isAnimated);
|
||||
}
|
||||
@@ -1604,11 +1606,11 @@
|
||||
this._element.style[dimension] = '';
|
||||
this._queueCallback(complete, this._element, true);
|
||||
}
|
||||
|
||||
// Private
|
||||
_isShown(element = this._element) {
|
||||
return element.classList.contains(CLASS_NAME_SHOW$7);
|
||||
}
|
||||
|
||||
// Private
|
||||
_configAfterMerge(config) {
|
||||
config.toggle = Boolean(config.toggle); // Coerce string values
|
||||
config.parent = getElement(config.parent);
|
||||
@@ -2666,7 +2668,6 @@
|
||||
var popperOffsets = computeOffsets({
|
||||
reference: referenceClientRect,
|
||||
element: popperRect,
|
||||
strategy: 'absolute',
|
||||
placement: placement
|
||||
});
|
||||
var popperClientRect = rectToClientRect(Object.assign({}, popperRect, popperOffsets));
|
||||
@@ -2994,7 +2995,6 @@
|
||||
state.modifiersData[name] = computeOffsets({
|
||||
reference: state.rects.reference,
|
||||
element: state.rects.popper,
|
||||
strategy: 'absolute',
|
||||
placement: state.placement
|
||||
});
|
||||
} // eslint-disable-next-line import/no-unused-modules
|
||||
@@ -3701,7 +3701,7 @@
|
||||
}
|
||||
_createPopper() {
|
||||
if (typeof Popper === 'undefined') {
|
||||
throw new TypeError('Bootstrap\'s dropdowns require Popper (https://popper.js.org)');
|
||||
throw new TypeError('Bootstrap\'s dropdowns require Popper (https://popper.js.org/docs/v2/)');
|
||||
}
|
||||
let referenceElement = this._element;
|
||||
if (this._config.reference === 'parent') {
|
||||
@@ -3780,7 +3780,7 @@
|
||||
}
|
||||
return {
|
||||
...defaultBsPopperConfig,
|
||||
...execute(this._config.popperConfig, [defaultBsPopperConfig])
|
||||
...execute(this._config.popperConfig, [undefined, defaultBsPopperConfig])
|
||||
};
|
||||
}
|
||||
_selectMenuItem({
|
||||
@@ -4802,7 +4802,6 @@
|
||||
*
|
||||
* Shout-out to Angular https://github.com/angular/angular/blob/15.2.8/packages/core/src/sanitization/url_sanitizer.ts#L38
|
||||
*/
|
||||
// eslint-disable-next-line unicorn/better-regex
|
||||
const SAFE_URL_PATTERN = /^(?!javascript:)(?:[a-z0-9+.-]+:|[^&:/?#]*(?:[/?#]|$))/i;
|
||||
const allowedAttribute = (attribute, allowedAttributeList) => {
|
||||
const attributeName = attribute.nodeName.toLowerCase();
|
||||
@@ -4967,7 +4966,7 @@
|
||||
return this._config.sanitize ? sanitizeHtml(arg, this._config.allowList, this._config.sanitizeFn) : arg;
|
||||
}
|
||||
_resolvePossibleFunction(arg) {
|
||||
return execute(arg, [this]);
|
||||
return execute(arg, [undefined, this]);
|
||||
}
|
||||
_putElementInTemplate(element, templateElement) {
|
||||
if (this._config.html) {
|
||||
@@ -5066,7 +5065,7 @@
|
||||
class Tooltip extends BaseComponent {
|
||||
constructor(element, config) {
|
||||
if (typeof Popper === 'undefined') {
|
||||
throw new TypeError('Bootstrap\'s tooltips require Popper (https://popper.js.org)');
|
||||
throw new TypeError('Bootstrap\'s tooltips require Popper (https://popper.js.org/docs/v2/)');
|
||||
}
|
||||
super(element, config);
|
||||
|
||||
@@ -5112,7 +5111,6 @@
|
||||
if (!this._isEnabled) {
|
||||
return;
|
||||
}
|
||||
this._activeTrigger.click = !this._activeTrigger.click;
|
||||
if (this._isShown()) {
|
||||
this._leave();
|
||||
return;
|
||||
@@ -5300,7 +5298,7 @@
|
||||
return offset;
|
||||
}
|
||||
_resolvePossibleFunction(arg) {
|
||||
return execute(arg, [this._element]);
|
||||
return execute(arg, [this._element, this._element]);
|
||||
}
|
||||
_getPopperConfig(attachment) {
|
||||
const defaultBsPopperConfig = {
|
||||
@@ -5338,7 +5336,7 @@
|
||||
};
|
||||
return {
|
||||
...defaultBsPopperConfig,
|
||||
...execute(this._config.popperConfig, [defaultBsPopperConfig])
|
||||
...execute(this._config.popperConfig, [undefined, defaultBsPopperConfig])
|
||||
};
|
||||
}
|
||||
_setListeners() {
|
||||
@@ -5347,6 +5345,7 @@
|
||||
if (trigger === 'click') {
|
||||
EventHandler.on(this._element, this.constructor.eventName(EVENT_CLICK$1), this._config.selector, event => {
|
||||
const context = this._initializeOnDelegatedTarget(event);
|
||||
context._activeTrigger[TRIGGER_CLICK] = !(context._isShown() && context._activeTrigger[TRIGGER_CLICK]);
|
||||
context.toggle();
|
||||
});
|
||||
} else if (trigger !== TRIGGER_MANUAL) {
|
||||
@@ -6212,7 +6211,6 @@
|
||||
}
|
||||
|
||||
// Private
|
||||
|
||||
_maybeScheduleHide() {
|
||||
if (!this._config.autohide) {
|
||||
return;
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,6 +1,6 @@
|
||||
/*!
|
||||
* Bootstrap v5.3.3 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2024 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)
|
||||
* Bootstrap v5.3.8 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2025 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||
*/
|
||||
import * as Popper from '@popperjs/core';
|
||||
@@ -201,7 +201,7 @@ const noop = () => {};
|
||||
* @param {HTMLElement} element
|
||||
* @return void
|
||||
*
|
||||
* @see https://www.charistheo.io/blog/2021/02/restart-a-css-animation-with-javascript/#restarting-a-css-animation
|
||||
* @see https://www.harrytheo.com/blog/2021/02/restart-a-css-animation-with-javascript/#restarting-a-css-animation
|
||||
*/
|
||||
const reflow = element => {
|
||||
element.offsetHeight; // eslint-disable-line no-unused-expressions
|
||||
@@ -246,7 +246,7 @@ const defineJQueryPlugin = plugin => {
|
||||
});
|
||||
};
|
||||
const execute = (possibleCallback, args = [], defaultValue = possibleCallback) => {
|
||||
return typeof possibleCallback === 'function' ? possibleCallback(...args) : defaultValue;
|
||||
return typeof possibleCallback === 'function' ? possibleCallback.call(...args) : defaultValue;
|
||||
};
|
||||
const executeAfterTransition = (callback, transitionElement, waitForTransition = true) => {
|
||||
if (!waitForTransition) {
|
||||
@@ -568,7 +568,7 @@ const Manipulator = {
|
||||
const bsKeys = Object.keys(element.dataset).filter(key => key.startsWith('bs') && !key.startsWith('bsConfig'));
|
||||
for (const key of bsKeys) {
|
||||
let pureKey = key.replace(/^bs/, '');
|
||||
pureKey = pureKey.charAt(0).toLowerCase() + pureKey.slice(1, pureKey.length);
|
||||
pureKey = pureKey.charAt(0).toLowerCase() + pureKey.slice(1);
|
||||
attributes[pureKey] = normalizeData(element.dataset[key]);
|
||||
}
|
||||
return attributes;
|
||||
@@ -643,7 +643,7 @@ class Config {
|
||||
* Constants
|
||||
*/
|
||||
|
||||
const VERSION = '5.3.3';
|
||||
const VERSION = '5.3.8';
|
||||
|
||||
/**
|
||||
* Class definition
|
||||
@@ -669,6 +669,8 @@ class BaseComponent extends Config {
|
||||
this[propertyName] = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Private
|
||||
_queueCallback(callback, element, isAnimated = true) {
|
||||
executeAfterTransition(callback, element, isAnimated);
|
||||
}
|
||||
@@ -1600,11 +1602,11 @@ class Collapse extends BaseComponent {
|
||||
this._element.style[dimension] = '';
|
||||
this._queueCallback(complete, this._element, true);
|
||||
}
|
||||
|
||||
// Private
|
||||
_isShown(element = this._element) {
|
||||
return element.classList.contains(CLASS_NAME_SHOW$7);
|
||||
}
|
||||
|
||||
// Private
|
||||
_configAfterMerge(config) {
|
||||
config.toggle = Boolean(config.toggle); // Coerce string values
|
||||
config.parent = getElement(config.parent);
|
||||
@@ -1858,7 +1860,7 @@ class Dropdown extends BaseComponent {
|
||||
}
|
||||
_createPopper() {
|
||||
if (typeof Popper === 'undefined') {
|
||||
throw new TypeError('Bootstrap\'s dropdowns require Popper (https://popper.js.org)');
|
||||
throw new TypeError('Bootstrap\'s dropdowns require Popper (https://popper.js.org/docs/v2/)');
|
||||
}
|
||||
let referenceElement = this._element;
|
||||
if (this._config.reference === 'parent') {
|
||||
@@ -1937,7 +1939,7 @@ class Dropdown extends BaseComponent {
|
||||
}
|
||||
return {
|
||||
...defaultBsPopperConfig,
|
||||
...execute(this._config.popperConfig, [defaultBsPopperConfig])
|
||||
...execute(this._config.popperConfig, [undefined, defaultBsPopperConfig])
|
||||
};
|
||||
}
|
||||
_selectMenuItem({
|
||||
@@ -2959,7 +2961,6 @@ const uriAttributes = new Set(['background', 'cite', 'href', 'itemtype', 'longde
|
||||
*
|
||||
* Shout-out to Angular https://github.com/angular/angular/blob/15.2.8/packages/core/src/sanitization/url_sanitizer.ts#L38
|
||||
*/
|
||||
// eslint-disable-next-line unicorn/better-regex
|
||||
const SAFE_URL_PATTERN = /^(?!javascript:)(?:[a-z0-9+.-]+:|[^&:/?#]*(?:[/?#]|$))/i;
|
||||
const allowedAttribute = (attribute, allowedAttributeList) => {
|
||||
const attributeName = attribute.nodeName.toLowerCase();
|
||||
@@ -3124,7 +3125,7 @@ class TemplateFactory extends Config {
|
||||
return this._config.sanitize ? sanitizeHtml(arg, this._config.allowList, this._config.sanitizeFn) : arg;
|
||||
}
|
||||
_resolvePossibleFunction(arg) {
|
||||
return execute(arg, [this]);
|
||||
return execute(arg, [undefined, this]);
|
||||
}
|
||||
_putElementInTemplate(element, templateElement) {
|
||||
if (this._config.html) {
|
||||
@@ -3223,7 +3224,7 @@ const DefaultType$3 = {
|
||||
class Tooltip extends BaseComponent {
|
||||
constructor(element, config) {
|
||||
if (typeof Popper === 'undefined') {
|
||||
throw new TypeError('Bootstrap\'s tooltips require Popper (https://popper.js.org)');
|
||||
throw new TypeError('Bootstrap\'s tooltips require Popper (https://popper.js.org/docs/v2/)');
|
||||
}
|
||||
super(element, config);
|
||||
|
||||
@@ -3269,7 +3270,6 @@ class Tooltip extends BaseComponent {
|
||||
if (!this._isEnabled) {
|
||||
return;
|
||||
}
|
||||
this._activeTrigger.click = !this._activeTrigger.click;
|
||||
if (this._isShown()) {
|
||||
this._leave();
|
||||
return;
|
||||
@@ -3457,7 +3457,7 @@ class Tooltip extends BaseComponent {
|
||||
return offset;
|
||||
}
|
||||
_resolvePossibleFunction(arg) {
|
||||
return execute(arg, [this._element]);
|
||||
return execute(arg, [this._element, this._element]);
|
||||
}
|
||||
_getPopperConfig(attachment) {
|
||||
const defaultBsPopperConfig = {
|
||||
@@ -3495,7 +3495,7 @@ class Tooltip extends BaseComponent {
|
||||
};
|
||||
return {
|
||||
...defaultBsPopperConfig,
|
||||
...execute(this._config.popperConfig, [defaultBsPopperConfig])
|
||||
...execute(this._config.popperConfig, [undefined, defaultBsPopperConfig])
|
||||
};
|
||||
}
|
||||
_setListeners() {
|
||||
@@ -3504,6 +3504,7 @@ class Tooltip extends BaseComponent {
|
||||
if (trigger === 'click') {
|
||||
EventHandler.on(this._element, this.constructor.eventName(EVENT_CLICK$1), this._config.selector, event => {
|
||||
const context = this._initializeOnDelegatedTarget(event);
|
||||
context._activeTrigger[TRIGGER_CLICK] = !(context._isShown() && context._activeTrigger[TRIGGER_CLICK]);
|
||||
context.toggle();
|
||||
});
|
||||
} else if (trigger !== TRIGGER_MANUAL) {
|
||||
@@ -4369,7 +4370,6 @@ class Toast extends BaseComponent {
|
||||
}
|
||||
|
||||
// Private
|
||||
|
||||
_maybeScheduleHide() {
|
||||
if (!this._config.autohide) {
|
||||
return;
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,6 +1,6 @@
|
||||
/*!
|
||||
* Bootstrap v5.3.3 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2024 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)
|
||||
* Bootstrap v5.3.8 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2025 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||
*/
|
||||
(function (global, factory) {
|
||||
@@ -224,7 +224,7 @@
|
||||
* @param {HTMLElement} element
|
||||
* @return void
|
||||
*
|
||||
* @see https://www.charistheo.io/blog/2021/02/restart-a-css-animation-with-javascript/#restarting-a-css-animation
|
||||
* @see https://www.harrytheo.com/blog/2021/02/restart-a-css-animation-with-javascript/#restarting-a-css-animation
|
||||
*/
|
||||
const reflow = element => {
|
||||
element.offsetHeight; // eslint-disable-line no-unused-expressions
|
||||
@@ -269,7 +269,7 @@
|
||||
});
|
||||
};
|
||||
const execute = (possibleCallback, args = [], defaultValue = possibleCallback) => {
|
||||
return typeof possibleCallback === 'function' ? possibleCallback(...args) : defaultValue;
|
||||
return typeof possibleCallback === 'function' ? possibleCallback.call(...args) : defaultValue;
|
||||
};
|
||||
const executeAfterTransition = (callback, transitionElement, waitForTransition = true) => {
|
||||
if (!waitForTransition) {
|
||||
@@ -591,7 +591,7 @@
|
||||
const bsKeys = Object.keys(element.dataset).filter(key => key.startsWith('bs') && !key.startsWith('bsConfig'));
|
||||
for (const key of bsKeys) {
|
||||
let pureKey = key.replace(/^bs/, '');
|
||||
pureKey = pureKey.charAt(0).toLowerCase() + pureKey.slice(1, pureKey.length);
|
||||
pureKey = pureKey.charAt(0).toLowerCase() + pureKey.slice(1);
|
||||
attributes[pureKey] = normalizeData(element.dataset[key]);
|
||||
}
|
||||
return attributes;
|
||||
@@ -666,7 +666,7 @@
|
||||
* Constants
|
||||
*/
|
||||
|
||||
const VERSION = '5.3.3';
|
||||
const VERSION = '5.3.8';
|
||||
|
||||
/**
|
||||
* Class definition
|
||||
@@ -692,6 +692,8 @@
|
||||
this[propertyName] = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Private
|
||||
_queueCallback(callback, element, isAnimated = true) {
|
||||
executeAfterTransition(callback, element, isAnimated);
|
||||
}
|
||||
@@ -1623,11 +1625,11 @@
|
||||
this._element.style[dimension] = '';
|
||||
this._queueCallback(complete, this._element, true);
|
||||
}
|
||||
|
||||
// Private
|
||||
_isShown(element = this._element) {
|
||||
return element.classList.contains(CLASS_NAME_SHOW$7);
|
||||
}
|
||||
|
||||
// Private
|
||||
_configAfterMerge(config) {
|
||||
config.toggle = Boolean(config.toggle); // Coerce string values
|
||||
config.parent = getElement(config.parent);
|
||||
@@ -1881,7 +1883,7 @@
|
||||
}
|
||||
_createPopper() {
|
||||
if (typeof Popper__namespace === 'undefined') {
|
||||
throw new TypeError('Bootstrap\'s dropdowns require Popper (https://popper.js.org)');
|
||||
throw new TypeError('Bootstrap\'s dropdowns require Popper (https://popper.js.org/docs/v2/)');
|
||||
}
|
||||
let referenceElement = this._element;
|
||||
if (this._config.reference === 'parent') {
|
||||
@@ -1960,7 +1962,7 @@
|
||||
}
|
||||
return {
|
||||
...defaultBsPopperConfig,
|
||||
...execute(this._config.popperConfig, [defaultBsPopperConfig])
|
||||
...execute(this._config.popperConfig, [undefined, defaultBsPopperConfig])
|
||||
};
|
||||
}
|
||||
_selectMenuItem({
|
||||
@@ -2982,7 +2984,6 @@
|
||||
*
|
||||
* Shout-out to Angular https://github.com/angular/angular/blob/15.2.8/packages/core/src/sanitization/url_sanitizer.ts#L38
|
||||
*/
|
||||
// eslint-disable-next-line unicorn/better-regex
|
||||
const SAFE_URL_PATTERN = /^(?!javascript:)(?:[a-z0-9+.-]+:|[^&:/?#]*(?:[/?#]|$))/i;
|
||||
const allowedAttribute = (attribute, allowedAttributeList) => {
|
||||
const attributeName = attribute.nodeName.toLowerCase();
|
||||
@@ -3147,7 +3148,7 @@
|
||||
return this._config.sanitize ? sanitizeHtml(arg, this._config.allowList, this._config.sanitizeFn) : arg;
|
||||
}
|
||||
_resolvePossibleFunction(arg) {
|
||||
return execute(arg, [this]);
|
||||
return execute(arg, [undefined, this]);
|
||||
}
|
||||
_putElementInTemplate(element, templateElement) {
|
||||
if (this._config.html) {
|
||||
@@ -3246,7 +3247,7 @@
|
||||
class Tooltip extends BaseComponent {
|
||||
constructor(element, config) {
|
||||
if (typeof Popper__namespace === 'undefined') {
|
||||
throw new TypeError('Bootstrap\'s tooltips require Popper (https://popper.js.org)');
|
||||
throw new TypeError('Bootstrap\'s tooltips require Popper (https://popper.js.org/docs/v2/)');
|
||||
}
|
||||
super(element, config);
|
||||
|
||||
@@ -3292,7 +3293,6 @@
|
||||
if (!this._isEnabled) {
|
||||
return;
|
||||
}
|
||||
this._activeTrigger.click = !this._activeTrigger.click;
|
||||
if (this._isShown()) {
|
||||
this._leave();
|
||||
return;
|
||||
@@ -3480,7 +3480,7 @@
|
||||
return offset;
|
||||
}
|
||||
_resolvePossibleFunction(arg) {
|
||||
return execute(arg, [this._element]);
|
||||
return execute(arg, [this._element, this._element]);
|
||||
}
|
||||
_getPopperConfig(attachment) {
|
||||
const defaultBsPopperConfig = {
|
||||
@@ -3518,7 +3518,7 @@
|
||||
};
|
||||
return {
|
||||
...defaultBsPopperConfig,
|
||||
...execute(this._config.popperConfig, [defaultBsPopperConfig])
|
||||
...execute(this._config.popperConfig, [undefined, defaultBsPopperConfig])
|
||||
};
|
||||
}
|
||||
_setListeners() {
|
||||
@@ -3527,6 +3527,7 @@
|
||||
if (trigger === 'click') {
|
||||
EventHandler.on(this._element, this.constructor.eventName(EVENT_CLICK$1), this._config.selector, event => {
|
||||
const context = this._initializeOnDelegatedTarget(event);
|
||||
context._activeTrigger[TRIGGER_CLICK] = !(context._isShown() && context._activeTrigger[TRIGGER_CLICK]);
|
||||
context.toggle();
|
||||
});
|
||||
} else if (trigger !== TRIGGER_MANUAL) {
|
||||
@@ -4392,7 +4393,6 @@
|
||||
}
|
||||
|
||||
// Private
|
||||
|
||||
_maybeScheduleHide() {
|
||||
if (!this._config.autohide) {
|
||||
return;
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user