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:
Lobstervova
2026-03-31 04:24:33 +03:00
31 changed files with 1512 additions and 2 deletions

View File

@@ -138,6 +138,11 @@
<artifactId>commons-io</artifactId>
<version>2.11.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.20.1</version> <!-- или актуальная версия -->
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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> {
}

View File

@@ -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> {
}

View File

@@ -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> {
}

View File

@@ -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> {
}

View File

@@ -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> {
}

View File

@@ -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> {
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -23,7 +23,7 @@ spring.flyway.locations=db/migration
spring.flyway.validate-migration-naming=true
spring.flyway.baseline-on-migrate=true
#====================== minio configuration =====================
minio.bucket=
minio.bucket=${spring.application.name}
minio.url=
minio.cdn=
minio.username=

View 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; }

View 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);
}

View 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; }

View 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);
}

View 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()
}

View File

@@ -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&amp;auto=format&amp;fit=crop&amp;w=800&amp;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>
`
}

View File

@@ -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>

View File

@@ -4,7 +4,7 @@ import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class DatePlannerApplicationTests {
class DatingPlannerApplicationTests {
@Test
void contextLoads() {