mirror of
https://github.com/LOBSTERVOVA/Tennis-Site.git
synced 2026-04-17 17:40:49 +03:00
Merge remote-tracking branch 'origin/master'
# Conflicts: # src/main/java/com/example/dateplanner/configurations/SecurityConfig.java # src/main/java/com/example/dateplanner/configurations/handlers/JwtAuthenticationManager.java # src/main/java/com/example/dateplanner/controllers/advice/SecurityAdviceController.java # src/main/java/com/example/dateplanner/controllers/web/AccountController.java # src/main/java/com/example/dateplanner/controllers/web/HomeController.java # src/main/java/com/example/dateplanner/models/entities/AppUser.java # src/main/java/com/example/dateplanner/models/enums/Role.java # src/main/java/com/example/dateplanner/services/JwtService.java # src/main/java/com/example/dateplanner/services/UserService.java # src/main/resources/db/migration/V1_0_1__init.sql # src/main/resources/static/css/style.css # src/main/resources/static/js/site/blocks/footer.js # src/main/resources/static/js/site/blocks/header.js # src/main/resources/static/js/site/pages/home/home.js # src/main/resources/templates/all-html.html # src/main/resources/templates/pages/home.html # src/main/resources/templates/template.html
This commit is contained in:
5
pom.xml
5
pom.xml
@@ -138,6 +138,11 @@
|
|||||||
<artifactId>commons-io</artifactId>
|
<artifactId>commons-io</artifactId>
|
||||||
<version>2.11.0</version>
|
<version>2.11.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.jackson.datatype</groupId>
|
||||||
|
<artifactId>jackson-datatype-jsr310</artifactId>
|
||||||
|
<version>2.20.1</version> <!-- или актуальная версия -->
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package com.example.dateplanner.models.entities;
|
||||||
|
|
||||||
|
import com.example.dateplanner.models.enums.DatingTime;
|
||||||
|
import com.example.dateplanner.models.enums.DatingType;
|
||||||
|
import com.example.dateplanner.models.enums.PriceType;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import org.springframework.data.annotation.Id;
|
||||||
|
import org.springframework.data.relational.core.mapping.Column;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@Table(name = "dating_places")
|
||||||
|
public class DatingPlace {
|
||||||
|
@Id
|
||||||
|
private UUID uuid;
|
||||||
|
private String title;
|
||||||
|
private String photo;
|
||||||
|
private String description;
|
||||||
|
@Column("organization_uuid")
|
||||||
|
private UUID organizationUuid;
|
||||||
|
@Column("dating_type")
|
||||||
|
private DatingType datingType;
|
||||||
|
private long price = 0;
|
||||||
|
@Column("price_type")
|
||||||
|
private PriceType priceType;
|
||||||
|
@Column("dating_time")
|
||||||
|
private DatingTime datingTime;
|
||||||
|
@Column("start_time")
|
||||||
|
private LocalDateTime startTime = null; // если это одноразовое событие
|
||||||
|
private Long duration = null; // длительность мероприятия
|
||||||
|
@Column("schedule_uuids")
|
||||||
|
private List<UUID> scheduleUuids = new ArrayList<>(); // если многоразовое событие
|
||||||
|
private String location;
|
||||||
|
private String coordinates;
|
||||||
|
private int views = 0; // просмотры
|
||||||
|
@Column("in_favourite")
|
||||||
|
private int inFavourite = 0;
|
||||||
|
private Double rating = null;
|
||||||
|
@Column("expiration_date")
|
||||||
|
private LocalDateTime expirationDate = null;
|
||||||
|
|
||||||
|
private boolean enabled = true;
|
||||||
|
|
||||||
|
@Column("created_at")
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
@Column("updated_at")
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
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 = "dating_plans")
|
||||||
|
public class DatingPlan {
|
||||||
|
@Id
|
||||||
|
private UUID uuid;
|
||||||
|
|
||||||
|
@Column("created_at")
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
@Column("updated_at")
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
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 = "dating_profiles")
|
||||||
|
public class DatingProfile {
|
||||||
|
@Id
|
||||||
|
private UUID uuid;
|
||||||
|
|
||||||
|
@Column("created_at")
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
@Column("updated_at")
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package com.example.dateplanner.models.entities;
|
||||||
|
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import org.springframework.data.annotation.Id;
|
||||||
|
import org.springframework.data.relational.core.mapping.Column;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@Table(name = "feedbacks")
|
||||||
|
public class Feedback {
|
||||||
|
@Id
|
||||||
|
private UUID uuid;
|
||||||
|
private UUID userUuid;
|
||||||
|
@Column("dating_place_uuid")
|
||||||
|
private UUID datingPlaceUuid;
|
||||||
|
private int feedback;
|
||||||
|
private String comment;
|
||||||
|
|
||||||
|
@Column("created_at")
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
@Column("updated_at")
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package com.example.dateplanner.models.entities;
|
||||||
|
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import org.springframework.data.annotation.Id;
|
||||||
|
import org.springframework.data.relational.core.mapping.Column;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@Table(name = "organizations")
|
||||||
|
public class Organization {
|
||||||
|
@Id
|
||||||
|
private UUID uuid;
|
||||||
|
private String logo;
|
||||||
|
private String title;
|
||||||
|
private String description; // хтмл код
|
||||||
|
@Column("owner_uuid")
|
||||||
|
private UUID ownerUuid;
|
||||||
|
|
||||||
|
@Column("expires_at")
|
||||||
|
private LocalDateTime expiresAt;
|
||||||
|
|
||||||
|
@Column("created_at")
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
@Column("updated_at")
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
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.Table;
|
||||||
|
import java.time.DayOfWeek;
|
||||||
|
import java.time.LocalTime;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@Table("schedules")
|
||||||
|
public class Schedule {
|
||||||
|
@Id
|
||||||
|
private UUID uuid;
|
||||||
|
private DayOfWeek dayOfWeek;
|
||||||
|
private LocalTime startTime;
|
||||||
|
private LocalTime endTime;
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.example.dateplanner.models.enums;
|
||||||
|
|
||||||
|
public enum DatingTime {
|
||||||
|
MORNING("Утро"),
|
||||||
|
DAY("День"),
|
||||||
|
EVENING("Вечер"),
|
||||||
|
NIGHT("Ночь"),
|
||||||
|
ANY("Любое");
|
||||||
|
|
||||||
|
private final String title;
|
||||||
|
|
||||||
|
DatingTime(String title){
|
||||||
|
this.title = title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTitle() {
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package com.example.dateplanner.models.enums;
|
||||||
|
|
||||||
|
public enum DatingType {
|
||||||
|
ROMANTIC("Романтическое"),
|
||||||
|
ACTIVE("Активное"),
|
||||||
|
INTELLECTUAL("Интеллектуальное"),
|
||||||
|
HORROR("Хоррор"),
|
||||||
|
COZY("Уют"),
|
||||||
|
MYSTERY("Тайна"),
|
||||||
|
CREATIVE("Творческое"),
|
||||||
|
OPENAIR("На открытом воздухе"),
|
||||||
|
GASTRONOMIC("Гастрономия");
|
||||||
|
|
||||||
|
private final String title;
|
||||||
|
DatingType(String title){
|
||||||
|
this.title = title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTitle() {
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.example.dateplanner.models.enums;
|
||||||
|
|
||||||
|
public enum PriceType {
|
||||||
|
ECONOMY("Эконом"),
|
||||||
|
AVERAGE("Средний"),
|
||||||
|
PREMIUM("Премиум");
|
||||||
|
|
||||||
|
private final String title;
|
||||||
|
|
||||||
|
PriceType(String title){
|
||||||
|
this.title = title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTitle() {
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package com.example.dateplanner.repositories;
|
||||||
|
|
||||||
|
import com.example.dateplanner.models.entities.DatingPlace;
|
||||||
|
import org.springframework.data.r2dbc.repository.R2dbcRepository;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public interface DatingPlaceRepository extends R2dbcRepository<DatingPlace, UUID> {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package com.example.dateplanner.repositories;
|
||||||
|
|
||||||
|
import com.example.dateplanner.models.entities.DatingPlan;
|
||||||
|
import org.springframework.data.r2dbc.repository.R2dbcRepository;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public interface DatingPlanRepository extends R2dbcRepository<DatingPlan, UUID> {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package com.example.dateplanner.repositories;
|
||||||
|
|
||||||
|
import com.example.dateplanner.models.entities.DatingProfile;
|
||||||
|
import org.springframework.data.r2dbc.repository.R2dbcRepository;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public interface DatingProfileRepository extends R2dbcRepository<DatingProfile, UUID> {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package com.example.dateplanner.repositories;
|
||||||
|
|
||||||
|
import com.example.dateplanner.models.entities.Feedback;
|
||||||
|
import org.springframework.data.r2dbc.repository.R2dbcRepository;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public interface FeedbackRepository extends R2dbcRepository<Feedback, UUID> {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package com.example.dateplanner.repositories;
|
||||||
|
|
||||||
|
import com.example.dateplanner.models.entities.Organization;
|
||||||
|
import org.springframework.data.r2dbc.repository.R2dbcRepository;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public interface OrganizationRepository extends R2dbcRepository<Organization, UUID> {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package com.example.dateplanner.repositories;
|
||||||
|
|
||||||
|
import com.example.dateplanner.models.entities.Schedule;
|
||||||
|
import org.springframework.data.r2dbc.repository.R2dbcRepository;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public interface ScheduleRepository extends R2dbcRepository<Schedule, UUID> {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package com.example.dateplanner.services;
|
||||||
|
|
||||||
|
import com.example.dateplanner.repositories.DatingPlaceRepository;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class DatingPlaceService {
|
||||||
|
private final DatingPlaceRepository datingPlaceRepository;
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package com.example.dateplanner.services;
|
||||||
|
|
||||||
|
import com.example.dateplanner.repositories.DatingPlanRepository;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class DatingPlanService {
|
||||||
|
private final DatingPlanRepository datingPlanRepository;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.example.dateplanner.services;
|
||||||
|
|
||||||
|
import com.example.dateplanner.repositories.DatingProfileRepository;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class DatingProfileService {
|
||||||
|
private final DatingProfileRepository datingProfileRepository;
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package com.example.dateplanner.services;
|
||||||
|
|
||||||
|
import com.example.dateplanner.repositories.FeedbackRepository;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class FeedbackService {
|
||||||
|
private final FeedbackRepository feedbackRepository;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package com.example.dateplanner.services;
|
||||||
|
|
||||||
|
import com.example.dateplanner.repositories.OrganizationRepository;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class OrganizationService {
|
||||||
|
private final OrganizationRepository organizationRepository;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.example.dateplanner.services;
|
||||||
|
|
||||||
|
import com.example.dateplanner.repositories.ScheduleRepository;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class ScheduleService {
|
||||||
|
private final ScheduleRepository scheduleRepository;
|
||||||
|
}
|
||||||
@@ -23,7 +23,7 @@ spring.flyway.locations=db/migration
|
|||||||
spring.flyway.validate-migration-naming=true
|
spring.flyway.validate-migration-naming=true
|
||||||
spring.flyway.baseline-on-migrate=true
|
spring.flyway.baseline-on-migrate=true
|
||||||
#====================== minio configuration =====================
|
#====================== minio configuration =====================
|
||||||
minio.bucket=
|
minio.bucket=${spring.application.name}
|
||||||
minio.url=
|
minio.url=
|
||||||
minio.cdn=
|
minio.cdn=
|
||||||
minio.username=
|
minio.username=
|
||||||
|
|||||||
124
src/main/resources/static/css/header.css
Normal file
124
src/main/resources/static/css/header.css
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
|
||||||
|
.user-avatar {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #e74c3c;
|
||||||
|
color: white;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 14px;
|
||||||
|
border: 2px solid white;
|
||||||
|
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info {
|
||||||
|
padding: 15px;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item {
|
||||||
|
padding: 10px 15px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item:hover {
|
||||||
|
background: rgba(231, 76, 60, 0.1) !important;
|
||||||
|
color: #e74c3c !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* Модальное окно авторизации */
|
||||||
|
.auth-tabs {
|
||||||
|
border: none ;
|
||||||
|
margin-bottom: 20px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-tabs .nav-link {
|
||||||
|
border: none !important;
|
||||||
|
color: #666;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 10px 0;
|
||||||
|
margin: 0 15px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-tabs .nav-link.active {
|
||||||
|
color: #e74c3c !important;
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-tabs .nav-link.active:after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 3px;
|
||||||
|
background: #e74c3c;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.forgot-password {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #e74c3c;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-divider {
|
||||||
|
text-align: center;
|
||||||
|
position: relative;
|
||||||
|
margin: 20px 0;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-divider:before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 1px;
|
||||||
|
background: #eee;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-divider span {
|
||||||
|
background: white;
|
||||||
|
padding: 0 15px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-auth-btn {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: white;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-auth-btn:hover {
|
||||||
|
border-color: #e74c3c;
|
||||||
|
background: rgba(231, 76, 60, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-auth-btn.vk { color: #4C75A3; }
|
||||||
|
.social-auth-btn.google { color: #DB4437; }
|
||||||
|
.social-auth-btn.yandex { color: #FF0000; }
|
||||||
129
src/main/resources/static/css/main.css
Normal file
129
src/main/resources/static/css/main.css
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
.btn-heart {
|
||||||
|
background: #e74c3c !important;
|
||||||
|
color: white !important;
|
||||||
|
border: none !important;
|
||||||
|
padding: 12px 30px !important;
|
||||||
|
border-radius: 50px !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
transition: all 0.3s !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-heart:hover {
|
||||||
|
background: #c0392b !important;
|
||||||
|
transform: translateY(-3px) !important;
|
||||||
|
box-shadow: 0 10px 20px rgba(231, 76, 60, 0.3) !important;
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Добавляем новые стили для иконки авторизации */
|
||||||
|
.auth-btn {
|
||||||
|
background: #e74c3c;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 50px;
|
||||||
|
padding: 8px 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: all 0.3s;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-btn:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 5px 15px rgba(231, 76, 60, 0.3);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.love-gradient {
|
||||||
|
background: linear-gradient(135deg, #e74c3c 0%, #fd79a8 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heart-animation {
|
||||||
|
position: absolute;
|
||||||
|
font-size: 30px;
|
||||||
|
color: rgba(255, 255, 255, 0.3);
|
||||||
|
animation: float 6s infinite ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes float {
|
||||||
|
0%, 100% { transform: translateY(0) rotate(0deg); }
|
||||||
|
50% { transform: translateY(-100px) rotate(180deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-title {
|
||||||
|
font-size: 3.5rem;
|
||||||
|
font-weight: 800;
|
||||||
|
text-shadow: 2px 2px 4px rgba(0,0,0,0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-grey {
|
||||||
|
color: #737373;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-romantic { border-color: #fd79a8; color: #fd79a8; }
|
||||||
|
.category-active { border-color: #2ecc71; color: #2ecc71; }
|
||||||
|
.category-intellectual { border-color: #3498db; color: #3498db; }
|
||||||
|
.category-horror { border-color: #2c3e50; color: #2c3e50; }
|
||||||
|
.category-cozy { border-color: #e67e22; color: #e67e22; }
|
||||||
|
.category-mystery { border-color: #9b59b6; color: #9b59b6; }
|
||||||
|
.category-luxury { border-color: #f1c40f; color: #f1c40f; }
|
||||||
|
.category-budget { border-color: #27ae60; color: #27ae60; }
|
||||||
|
|
||||||
|
.filter-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 25px;
|
||||||
|
box-shadow: 0 10px 30px rgba(0,0,0,0.08);
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-btn {
|
||||||
|
border: 2px solid transparent;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 50px;
|
||||||
|
margin: 5px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-btn:hover, .filter-btn.active {
|
||||||
|
transform: translateY(-3px);
|
||||||
|
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-btn.active {
|
||||||
|
border-color: #e74c3c;
|
||||||
|
color: #e74c3c;
|
||||||
|
background: rgba(231, 76, 60, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
footer {
|
||||||
|
background: #2d3436;
|
||||||
|
color: white;
|
||||||
|
padding: 60px 0 30px;
|
||||||
|
margin-top: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-icon {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
background: rgba(255,255,255,0.1);
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-right: 10px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-icon:hover {
|
||||||
|
background: var(--love-red);
|
||||||
|
transform: translateY(-3px);
|
||||||
|
}
|
||||||
73
src/main/resources/static/css/places.css
Normal file
73
src/main/resources/static/css/places.css
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
.place-card {
|
||||||
|
border: none;
|
||||||
|
border-radius: 20px;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: transform 0.3s, box-shadow 0.3s;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.place-card:hover {
|
||||||
|
transform: translateY(-10px);
|
||||||
|
box-shadow: 0 20px 40px rgba(0,0,0,0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.place-img {
|
||||||
|
height: 200px;
|
||||||
|
object-fit: cover;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.place-category {
|
||||||
|
position: absolute;
|
||||||
|
top: 15px;
|
||||||
|
right: 15px;
|
||||||
|
padding: 5px 15px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heart-counter {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
left: 10px;
|
||||||
|
background: rgba(255,255,255,0.9);
|
||||||
|
padding: 5px 10px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heart-counter i {
|
||||||
|
color: #e74c3c;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rating {
|
||||||
|
color: #f1c40f;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-tag {
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
color: #e74c3c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.place-category {
|
||||||
|
position: absolute;
|
||||||
|
top: 15px;
|
||||||
|
right: 15px;
|
||||||
|
padding: 5px 15px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.romantic-badge { background: #FD79A8FF; color: white; }
|
||||||
|
.active-badge { background: #2ECC71FF; color: white; }
|
||||||
|
.intellectual-badge { background: #3498DBFF; color: white; }
|
||||||
|
.horror-badge { background: #2C3E50FF; color: white; }
|
||||||
|
.cozy-badge { background: #E67E22FF; color: white; }
|
||||||
|
.mystery-badge { background: #9B59B6FF; color: white; }
|
||||||
95
src/main/resources/static/js/site/blocks/place-filters.js
Normal file
95
src/main/resources/static/js/site/blocks/place-filters.js
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
let currentFilters = {category: [], price: [], time: []};
|
||||||
|
export function initPlaceFilters($filtersContainer) {
|
||||||
|
console.log("init places filters");
|
||||||
|
|
||||||
|
// Одна общая функция для всех фильтров
|
||||||
|
function toggleFilter(event) {
|
||||||
|
const $btn = $(event.currentTarget);
|
||||||
|
$btn.toggleClass('active');
|
||||||
|
|
||||||
|
// Можно получить тип и значение фильтра
|
||||||
|
const type = $btn.data('type');
|
||||||
|
const value = $btn.data('value');
|
||||||
|
|
||||||
|
console.log(`Фильтр: ${type} = ${value}, активен: ${$btn.hasClass('active')}`);
|
||||||
|
|
||||||
|
// ОБНОВЛЯЕМ МАССИВ ФИЛЬТРОВ!
|
||||||
|
if ($btn.hasClass('active')) {
|
||||||
|
// Добавляем фильтр, если его еще нет
|
||||||
|
if (!currentFilters[type].includes(value)) {
|
||||||
|
currentFilters[type].push(value);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Удаляем фильтр
|
||||||
|
currentFilters[type] = currentFilters[type].filter(item => item !== value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Здесь можно обновить данные или сделать запрос
|
||||||
|
// Триггерим событие изменения фильтров
|
||||||
|
$(document).trigger('filtersChanged', [currentFilters]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const $filters = $(`
|
||||||
|
<section class="py-5">
|
||||||
|
<div class="container">
|
||||||
|
<div class="filter-card">
|
||||||
|
<h3 class="mb-4">Какое свидание вы ищете?</h3>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h5 class="mb-3">Тип свидания</h5>
|
||||||
|
<div class="d-flex flex-wrap mb-4">
|
||||||
|
<div class="filter-btn category-romantic" data-type="category" data-value="ROMANTIC">
|
||||||
|
<i class="fas fa-heart me-2"></i>Романтическое
|
||||||
|
</div>
|
||||||
|
<div class="filter-btn category-active" data-type="category" data-value="ACTIVE">
|
||||||
|
<i class="fas fa-hiking me-2"></i>Активное
|
||||||
|
</div>
|
||||||
|
<div class="filter-btn category-intellectual" data-type="category" data-value="INTELLECTUAL">
|
||||||
|
<i class="fas fa-brain me-2"></i>Интеллектуальное
|
||||||
|
</div>
|
||||||
|
<div class="filter-btn category-horror" data-type="category" data-value="HORROR">
|
||||||
|
<i class="fas fa-ghost me-2"></i>Хоррор
|
||||||
|
</div>
|
||||||
|
<div class="filter-btn category-cozy" data-type="category" data-value="COZY">
|
||||||
|
<i class="fas fa-mug-hot me-2"></i>Уютное
|
||||||
|
</div>
|
||||||
|
<div class="filter-btn category-mystery" data-type="category" data-value="MYSTERY">
|
||||||
|
<i class="fas fa-search me-2"></i>Тайна
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h5 class="mb-3">Бюджет</h5>
|
||||||
|
<div class="d-flex flex-wrap mb-4">
|
||||||
|
<div class="filter-btn category-budget" data-type="price" data-value="BUDGET">
|
||||||
|
<i class="fas fa-wallet me-2"></i>Эконом (до 2,000₽)
|
||||||
|
</div>
|
||||||
|
<div class="filter-btn category-luxury" data-type="price" data-value="LUXURY">
|
||||||
|
<i class="fas fa-gem me-2"></i>Премиум (от 5,000₽)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h5 class="mb-3">Время</h5>
|
||||||
|
<div class="d-flex flex-wrap">
|
||||||
|
<div class="filter-btn" data-type="time" data-value="DAY">
|
||||||
|
<i class="fas fa-sun me-2"></i>День
|
||||||
|
</div>
|
||||||
|
<div class="filter-btn" data-type="time" data-value="EVENING">
|
||||||
|
<i class="fas fa-moon me-2"></i>Вечер
|
||||||
|
</div>
|
||||||
|
<div class="filter-btn" data-type="time" data-value="NIGHT">
|
||||||
|
<i class="fas fa-star me-2"></i>Ночь
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Вешаем один обработчик на все кнопки
|
||||||
|
$filters.on('click', '.filter-btn', toggleFilter);
|
||||||
|
|
||||||
|
$filtersContainer.append($filters);
|
||||||
|
}
|
||||||
35
src/main/resources/static/js/site/blocks/places-block.js
Normal file
35
src/main/resources/static/js/site/blocks/places-block.js
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import {createCard} from "./single-place-card.js";
|
||||||
|
|
||||||
|
export function initPlacesBlock($container) {
|
||||||
|
console.log("initPlacesBlock")
|
||||||
|
$container.append(
|
||||||
|
$(`
|
||||||
|
<div class="container">
|
||||||
|
<div class="row" id="placesContainer">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`)
|
||||||
|
)
|
||||||
|
|
||||||
|
function loadPlaces(filters = {}) {
|
||||||
|
$('#placesContainer').empty().append('<div class="text-center">Загрузка...</div>');
|
||||||
|
|
||||||
|
// Задержка 500ms перед отрисовкой
|
||||||
|
setTimeout(() => {
|
||||||
|
$('#placesContainer').empty();
|
||||||
|
console.log("новые фильтры в loadPlaces: ", filters)
|
||||||
|
for (let i = 0; i < 10; i++) {
|
||||||
|
$('#placesContainer').append($(createCard()));
|
||||||
|
}
|
||||||
|
}, 1500);
|
||||||
|
}
|
||||||
|
|
||||||
|
$(document).on('filtersChanged', (e, filters) => {
|
||||||
|
console.log('Фильтры изменились:', filters);
|
||||||
|
loadPlaces(filters);
|
||||||
|
});
|
||||||
|
|
||||||
|
// первая загрузка
|
||||||
|
loadPlaces()
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
export function createCard() {
|
||||||
|
return `
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card place-card">
|
||||||
|
<div class="position-relative">
|
||||||
|
<img src="https://images.unsplash.com/photo-1517248135467-4c7edcad34c4?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80" class="place-img" alt="Ресторан 'Панорама'">
|
||||||
|
<div class="place-category romantic-badge">Романтическое</div>
|
||||||
|
<div class="heart-counter">
|
||||||
|
<i class="fas fa-heart"></i> 128
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">Ресторан 'Панорама'</h5>
|
||||||
|
<p class="card-text text-muted">Ресторан на 25-м этаже с панорамным видом на город. Идеальное место для романтического ужина при све...</p>
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<div>
|
||||||
|
<span class="rating">
|
||||||
|
★★★★½
|
||||||
|
<small class="text-muted ms-1">4.8</small>
|
||||||
|
</span>
|
||||||
|
<div class="price-tag mt-1">4500₽</div>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-heart btn-sm" onclick="viewPlaceDetails(1)">
|
||||||
|
<i class="fas fa-info-circle me-1"></i>Подробнее
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
}
|
||||||
@@ -0,0 +1,652 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>DatePlanner - Идеальные места для свиданий</title>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
||||||
|
<style>
|
||||||
|
/* Оставляем все существующие стили */
|
||||||
|
:root {
|
||||||
|
--love-red: #e74c3c;
|
||||||
|
--romantic-pink: #fd79a8;
|
||||||
|
--cozy-orange: #e67e22;
|
||||||
|
--intellectual-blue: #3498db;
|
||||||
|
--adventure-green: #2ecc71;
|
||||||
|
--mystery-purple: #9b59b6;
|
||||||
|
--dark-mode: #2d3436;
|
||||||
|
--light-bg: #f9f7f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ... все остальные стили без изменений ... */
|
||||||
|
|
||||||
|
/* Добавляем новые стили для иконки авторизации */
|
||||||
|
.auth-btn {
|
||||||
|
background: linear-gradient(135deg, var(--love-red) 0%, var(--romantic-pink) 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 50px;
|
||||||
|
padding: 8px 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: all 0.3s;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-btn:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 5px 15px rgba(231, 76, 60, 0.3);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-btn-outline {
|
||||||
|
background: transparent;
|
||||||
|
border: 2px solid var(--love-red);
|
||||||
|
color: var(--love-red);
|
||||||
|
padding: 8px 20px;
|
||||||
|
border-radius: 50px;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: all 0.3s;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-btn-outline:hover {
|
||||||
|
background: var(--love-red);
|
||||||
|
color: white;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-avatar {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: linear-gradient(135deg, var(--love-red) 0%, var(--romantic-pink) 100%);
|
||||||
|
color: white;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 14px;
|
||||||
|
border: 2px solid white;
|
||||||
|
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-user {
|
||||||
|
min-width: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info {
|
||||||
|
padding: 15px;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-details h6 {
|
||||||
|
margin: 0;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-details small {
|
||||||
|
color: #666;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item {
|
||||||
|
padding: 10px 15px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item:hover {
|
||||||
|
background: rgba(231, 76, 60, 0.1);
|
||||||
|
color: var(--love-red);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-divider {
|
||||||
|
margin: 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Модальное окно авторизации */
|
||||||
|
.auth-tabs {
|
||||||
|
border: none;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-tabs .nav-link {
|
||||||
|
border: none;
|
||||||
|
color: #666;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 10px 0;
|
||||||
|
margin: 0 15px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-tabs .nav-link.active {
|
||||||
|
color: var(--love-red);
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-tabs .nav-link.active:after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 3px;
|
||||||
|
background: var(--love-red);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-auth-btn {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: white;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-auth-btn:hover {
|
||||||
|
border-color: var(--love-red);
|
||||||
|
background: rgba(231, 76, 60, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-auth-btn.vk { color: #4C75A3; }
|
||||||
|
.social-auth-btn.google { color: #DB4437; }
|
||||||
|
.social-auth-btn.yandex { color: #FF0000; }
|
||||||
|
|
||||||
|
.auth-divider {
|
||||||
|
text-align: center;
|
||||||
|
position: relative;
|
||||||
|
margin: 20px 0;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-divider:before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 1px;
|
||||||
|
background: #eee;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-divider span {
|
||||||
|
background: white;
|
||||||
|
padding: 0 15px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-check-label {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.forgot-password {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--love-red);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.forgot-password:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Адаптивность */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.auth-btn-text {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-btn, .auth-btn-outline {
|
||||||
|
padding: 8px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-user {
|
||||||
|
min-width: 200px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Сообщение об успешной авторизации */
|
||||||
|
.auth-success {
|
||||||
|
display: none;
|
||||||
|
background: #d4edda;
|
||||||
|
color: #155724;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border: 1px solid #c3e6cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-success.show {
|
||||||
|
display: block;
|
||||||
|
animation: fadeIn 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; transform: translateY(-10px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Навигация с добавленной кнопкой авторизации -->
|
||||||
|
<nav class="navbar navbar-expand-lg navbar-light bg-white shadow-sm fixed-top">
|
||||||
|
<div class="container">
|
||||||
|
<a class="navbar-brand" href="#">
|
||||||
|
<i class="fas fa-heart heart-icon me-2"></i>DatePlanner
|
||||||
|
</a>
|
||||||
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
<div class="collapse navbar-collapse" id="navbarNav">
|
||||||
|
<ul class="navbar-nav ms-auto">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="#places">Места</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="#map-section">Карта</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="#ideas">Идеи</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="#admin">Для организаций</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<!-- Кнопка авторизации (показывается когда пользователь не авторизован) -->
|
||||||
|
<div id="authButtonContainer" class="d-flex align-items-center ms-3">
|
||||||
|
<button class="auth-btn" onclick="showAuthModal()">
|
||||||
|
<i class="fas fa-user"></i>
|
||||||
|
<span class="auth-btn-text">Войти</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Профиль пользователя (показывается после авторизации) -->
|
||||||
|
<div id="userProfileContainer" class="dropdown ms-3" style="display: none;">
|
||||||
|
<a href="#" class="d-flex align-items-center text-decoration-none dropdown-toggle"
|
||||||
|
data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
<div class="user-avatar" id="userAvatar">А</div>
|
||||||
|
<span class="ms-2 d-none d-md-inline" id="userName">Алексей</span>
|
||||||
|
</a>
|
||||||
|
<ul class="dropdown-menu dropdown-user">
|
||||||
|
<li>
|
||||||
|
<div class="user-info">
|
||||||
|
<div class="user-avatar" id="dropdownUserAvatar">А</div>
|
||||||
|
<div class="user-details">
|
||||||
|
<h6 id="dropdownUserName">Алексей Петров</h6>
|
||||||
|
<small id="dropdownUserEmail">alexey@example.com</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li><hr class="dropdown-divider"></li>
|
||||||
|
<li><a class="dropdown-item" href="#"><i class="fas fa-heart"></i> Мои избранные</a></li>
|
||||||
|
<li><a class="dropdown-item" href="#"><i class="fas fa-calendar-alt"></i> Мои бронирования</a></li>
|
||||||
|
<li><a class="dropdown-item" href="#"><i class="fas fa-star"></i> Мои отзывы</a></li>
|
||||||
|
<li><hr class="dropdown-divider"></li>
|
||||||
|
<li><a class="dropdown-item" href="#" onclick="showAddPlaceModal()"><i class="fas fa-plus-circle"></i> Добавить место</a></li>
|
||||||
|
<li><hr class="dropdown-divider"></li>
|
||||||
|
<li><a class="dropdown-item" href="#"><i class="fas fa-cog"></i> Настройки</a></li>
|
||||||
|
<li><a class="dropdown-item text-danger" href="#" onclick="logout()"><i class="fas fa-sign-out-alt"></i> Выйти</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="btn btn-heart ms-3" onclick="checkAuthBeforeAddPlace()">
|
||||||
|
<i class="fas fa-plus me-2"></i>Добавить место
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- Герой секция (оставляем без изменений) -->
|
||||||
|
<section class="love-gradient hero-section">
|
||||||
|
<!-- ... существующий код герой секции без изменений ... -->
|
||||||
|
<div class="heart-animation" style="top:10%;left:5%">❤️</div>
|
||||||
|
<div class="heart-animation" style="top:30%;right:10%">❤️</div>
|
||||||
|
<div class="heart-animation" style="bottom:20%;left:15%">❤️</div>
|
||||||
|
<div class="heart-animation" style="bottom:40%;right:20%">❤️</div>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="row align-items-center">
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<h1 class="hero-title mb-4">Найдите идеальное место для свидания</h1>
|
||||||
|
<p class="lead mb-4">Более 500 проверенных локаций: от романтических ужинов до экстремальных приключений. Подберите свидание по настроению, бюджету и интересам.</p>
|
||||||
|
<div class="d-flex flex-wrap gap-3">
|
||||||
|
<a href="#places" class="btn btn-heart">
|
||||||
|
<i class="fas fa-search me-2"></i>Найти место
|
||||||
|
</a>
|
||||||
|
<a href="#map-section" class="btn btn-outline-light">
|
||||||
|
<i class="fas fa-map-marked-alt me-2"></i>Посмотреть на карте
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<img src="https://images.unsplash.com/photo-1518568814500-bf0f8d125f46?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80"
|
||||||
|
class="img-fluid rounded-3 shadow-lg"
|
||||||
|
alt="Свидание в парке">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Остальные секции остаются без изменений -->
|
||||||
|
<!-- Фильтры -->
|
||||||
|
<section class="py-5 bg-light">
|
||||||
|
<!-- ... существующий код фильтров ... -->
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Модальное окно авторизации -->
|
||||||
|
<div class="modal fade" id="authModal" tabindex="-1" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header border-0">
|
||||||
|
<h5 class="modal-title">Вход в DatePlanner</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body p-4">
|
||||||
|
<div id="authSuccess" class="auth-success">
|
||||||
|
<i class="fas fa-check-circle me-2"></i>
|
||||||
|
Вы успешно вошли в систему!
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul class="nav nav-tabs auth-tabs" id="authTab" role="tablist">
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link active" id="login-tab" data-bs-toggle="tab" data-bs-target="#login" type="button">
|
||||||
|
Вход
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link" id="register-tab" data-bs-toggle="tab" data-bs-target="#register" type="button">
|
||||||
|
Регистрация
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="tab-content" id="authTabContent">
|
||||||
|
<!-- Форма входа -->
|
||||||
|
<div class="tab-pane fade show active" id="login" role="tabpanel">
|
||||||
|
<form id="loginForm">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Email или телефон</label>
|
||||||
|
<input type="text" class="form-control" id="loginEmail" placeholder="example@mail.ru" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Пароль</label>
|
||||||
|
<input type="password" class="form-control" id="loginPassword" required>
|
||||||
|
<div class="text-end mt-1">
|
||||||
|
<a href="#" class="forgot-password" onclick="showForgotPassword()">Забыли пароль?</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3 form-check">
|
||||||
|
<input type="checkbox" class="form-check-input" id="rememberMe">
|
||||||
|
<label class="form-check-label" for="rememberMe">Запомнить меня</label>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-heart w-100">Войти</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="auth-divider">
|
||||||
|
<span>или войдите через</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="social-auth-btn vk" onclick="loginWithVK()">
|
||||||
|
<i class="fab fa-vk"></i> ВКонтакте
|
||||||
|
</button>
|
||||||
|
<button class="social-auth-btn google" onclick="loginWithGoogle()">
|
||||||
|
<i class="fab fa-google"></i> Google
|
||||||
|
</button>
|
||||||
|
<button class="social-auth-btn yandex" onclick="loginWithYandex()">
|
||||||
|
<i class="fab fa-yandex"></i> Яндекс
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Форма регистрации -->
|
||||||
|
<div class="tab-pane fade" id="register" role="tabpanel">
|
||||||
|
<form id="registerForm">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label class="form-label">Имя</label>
|
||||||
|
<input type="text" class="form-control" id="registerFirstName" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label class="form-label">Фамилия</label>
|
||||||
|
<input type="text" class="form-control" id="registerLastName">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Email</label>
|
||||||
|
<input type="email" class="form-control" id="registerEmail" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Пароль</label>
|
||||||
|
<input type="password" class="form-control" id="registerPassword" required>
|
||||||
|
<small class="text-muted">Минимум 6 символов</small>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Подтвердите пароль</label>
|
||||||
|
<input type="password" class="form-control" id="registerPasswordConfirm" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3 form-check">
|
||||||
|
<input type="checkbox" class="form-check-input" id="acceptTerms" required>
|
||||||
|
<label class="form-check-label" for="acceptTerms">
|
||||||
|
Я принимаю <a href="#" class="forgot-password">условия использования</a> и
|
||||||
|
<a href="#" class="forgot-password">политику конфиденциальности</a>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-heart w-100">Зарегистрироваться</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Скрипты -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||||
|
<script>
|
||||||
|
// Инициализация модального окна авторизации
|
||||||
|
const authModal = new bootstrap.Modal(document.getElementById('authModal'));
|
||||||
|
let isAuthenticated = false; // Флаг авторизации
|
||||||
|
|
||||||
|
// Показать модальное окно авторизации
|
||||||
|
function showAuthModal() {
|
||||||
|
authModal.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверка авторизации перед добавлением места
|
||||||
|
function checkAuthBeforeAddPlace() {
|
||||||
|
if (isAuthenticated) {
|
||||||
|
showAddPlaceModal();
|
||||||
|
} else {
|
||||||
|
showAuthModal();
|
||||||
|
// Переключаем на вкладку регистрации, если пользователь хочет добавить место
|
||||||
|
setTimeout(() => {
|
||||||
|
const registerTab = document.getElementById('register-tab');
|
||||||
|
if (registerTab) {
|
||||||
|
registerTab.click();
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обработка формы входа
|
||||||
|
document.getElementById('loginForm').addEventListener('submit', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const email = document.getElementById('loginEmail').value;
|
||||||
|
const password = document.getElementById('loginPassword').value;
|
||||||
|
|
||||||
|
// Здесь должна быть реальная проверка на сервере
|
||||||
|
// Для демо просто имитируем успешный вход
|
||||||
|
if (email && password) {
|
||||||
|
// Симуляция успешного входа
|
||||||
|
loginUser({
|
||||||
|
firstName: 'Алексей',
|
||||||
|
lastName: 'Петров',
|
||||||
|
email: email,
|
||||||
|
avatarLetter: 'А'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Показать сообщение об успехе
|
||||||
|
document.getElementById('authSuccess').classList.add('show');
|
||||||
|
|
||||||
|
// Закрыть модальное окно через 2 секунды
|
||||||
|
setTimeout(() => {
|
||||||
|
authModal.hide();
|
||||||
|
document.getElementById('authSuccess').classList.remove('show');
|
||||||
|
this.reset();
|
||||||
|
}, 2000);
|
||||||
|
} else {
|
||||||
|
alert('Пожалуйста, заполните все поля');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Обработка формы регистрации
|
||||||
|
document.getElementById('registerForm').addEventListener('submit', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const firstName = document.getElementById('registerFirstName').value;
|
||||||
|
const lastName = document.getElementById('registerLastName').value;
|
||||||
|
const email = document.getElementById('registerEmail').value;
|
||||||
|
const password = document.getElementById('registerPassword').value;
|
||||||
|
const passwordConfirm = document.getElementById('registerPasswordConfirm').value;
|
||||||
|
|
||||||
|
if (password !== passwordConfirm) {
|
||||||
|
alert('Пароли не совпадают');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password.length < 6) {
|
||||||
|
alert('Пароль должен содержать минимум 6 символов');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Здесь должна быть реальная регистрация на сервере
|
||||||
|
// Для демо просто имитируем успешную регистрацию и вход
|
||||||
|
loginUser({
|
||||||
|
firstName: firstName,
|
||||||
|
lastName: lastName,
|
||||||
|
email: email,
|
||||||
|
avatarLetter: firstName.charAt(0).toUpperCase()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Показать сообщение об успехе
|
||||||
|
document.getElementById('authSuccess').classList.add('show');
|
||||||
|
|
||||||
|
// Закрыть модальное окно через 2 секунды
|
||||||
|
setTimeout(() => {
|
||||||
|
authModal.hide();
|
||||||
|
document.getElementById('authSuccess').classList.remove('show');
|
||||||
|
this.reset();
|
||||||
|
}, 2000);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Функция входа пользователя
|
||||||
|
function loginUser(userData) {
|
||||||
|
isAuthenticated = true;
|
||||||
|
|
||||||
|
// Обновляем интерфейс
|
||||||
|
document.getElementById('authButtonContainer').style.display = 'none';
|
||||||
|
document.getElementById('userProfileContainer').style.display = 'block';
|
||||||
|
|
||||||
|
// Заполняем данные пользователя
|
||||||
|
const fullName = userData.firstName + (userData.lastName ? ' ' + userData.lastName : '');
|
||||||
|
document.getElementById('userName').textContent = userData.firstName;
|
||||||
|
document.getElementById('userAvatar').textContent = userData.avatarLetter;
|
||||||
|
document.getElementById('dropdownUserName').textContent = fullName;
|
||||||
|
document.getElementById('dropdownUserEmail').textContent = userData.email;
|
||||||
|
document.getElementById('dropdownUserAvatar').textContent = userData.avatarLetter;
|
||||||
|
|
||||||
|
// Сохраняем в localStorage (в реальном приложении используйте токены)
|
||||||
|
localStorage.setItem('datePlannerUser', JSON.stringify(userData));
|
||||||
|
|
||||||
|
// Показать приветственное сообщение
|
||||||
|
showWelcomeMessage(userData.firstName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Функция выхода
|
||||||
|
function logout() {
|
||||||
|
isAuthenticated = false;
|
||||||
|
|
||||||
|
// Обновляем интерфейс
|
||||||
|
document.getElementById('authButtonContainer').style.display = 'block';
|
||||||
|
document.getElementById('userProfileContainer').style.display = 'none';
|
||||||
|
|
||||||
|
// Очищаем localStorage
|
||||||
|
localStorage.removeItem('datePlannerUser');
|
||||||
|
|
||||||
|
// Показать сообщение о выходе
|
||||||
|
alert('Вы успешно вышли из системы');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверка авторизации при загрузке страницы
|
||||||
|
function checkAuthOnLoad() {
|
||||||
|
const savedUser = localStorage.getItem('datePlannerUser');
|
||||||
|
if (savedUser) {
|
||||||
|
loginUser(JSON.parse(savedUser));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Показать приветственное сообщение
|
||||||
|
function showWelcomeMessage(firstName) {
|
||||||
|
// Создаем и показываем тост-уведомление
|
||||||
|
const toast = document.createElement('div');
|
||||||
|
toast.className = 'position-fixed top-0 end-0 p-3';
|
||||||
|
toast.style.zIndex = '1060';
|
||||||
|
toast.innerHTML = `
|
||||||
|
<div class="toast show" role="alert">
|
||||||
|
<div class="toast-header" style="background: linear-gradient(135deg, #e74c3c 0%, #fd79a8 100%); color: white;">
|
||||||
|
<i class="fas fa-heart me-2"></i>
|
||||||
|
<strong class="me-auto">DatePlanner</strong>
|
||||||
|
<button type="button" class="btn-close btn-close-white" onclick="this.closest('.toast').remove()"></button>
|
||||||
|
</div>
|
||||||
|
<div class="toast-body">
|
||||||
|
Добро пожаловать, ${firstName}! 🎉<br>
|
||||||
|
Теперь вы можете добавлять места в избранное и бронировать свидания.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
document.body.appendChild(toast);
|
||||||
|
|
||||||
|
// Автоматически удаляем через 5 секунд
|
||||||
|
setTimeout(() => {
|
||||||
|
if (toast.parentNode) {
|
||||||
|
toast.remove();
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Инициализация при загрузке страницы
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
checkAuthOnLoad();
|
||||||
|
|
||||||
|
// Инициализация карты и остальной функциональности
|
||||||
|
// ... существующий код инициализации ...
|
||||||
|
|
||||||
|
// Ваш существующий код здесь
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -4,7 +4,7 @@ import org.junit.jupiter.api.Test;
|
|||||||
import org.springframework.boot.test.context.SpringBootTest;
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
|
||||||
@SpringBootTest
|
@SpringBootTest
|
||||||
class DatePlannerApplicationTests {
|
class DatingPlannerApplicationTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void contextLoads() {
|
void contextLoads() {
|
||||||
Reference in New Issue
Block a user