mirror of
https://github.com/LOBSTERVOVA/Tennis-Site.git
synced 2026-04-17 17:40:49 +03:00
отработано создание организации в профиле
This commit is contained in:
@@ -4,10 +4,8 @@ import com.example.dateplanner.models.entities.AppUser;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.PropertySource;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.security.web.csrf.CsrfToken;
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@ControllerAdvice
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
package com.example.dateplanner.controllers.web;
|
||||
|
||||
import ch.qos.logback.core.model.Model;
|
||||
import com.example.dateplanner.models.entities.AppUser;
|
||||
import com.example.dateplanner.services.UserService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.reactive.result.view.Rendering;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@@ -21,15 +21,27 @@ import java.util.Map;
|
||||
@RequestMapping("/account")
|
||||
public class AccountController {
|
||||
|
||||
private final UserService userService;
|
||||
|
||||
@GetMapping("/profile")
|
||||
public Mono<Rendering> profile() {
|
||||
Map<String, Object> model = new HashMap<>();
|
||||
model.put("title", "Profile");
|
||||
model.put("index", "profile");
|
||||
return Mono.just(
|
||||
Rendering.view("template")
|
||||
.model(model)
|
||||
.build()
|
||||
);
|
||||
return Mono.just(Rendering.view("template").model(model).build());
|
||||
}
|
||||
|
||||
@PutMapping("/profile")
|
||||
@ResponseBody
|
||||
public Mono<ResponseEntity<AppUser>> updateProfile(
|
||||
@AuthenticationPrincipal AppUser currentUser,
|
||||
@RequestBody UpdateProfileRequest request) {
|
||||
if (currentUser == null) return Mono.just(ResponseEntity.status(401).build());
|
||||
currentUser.setFirstName(request.firstName());
|
||||
currentUser.setLastName(request.lastName());
|
||||
currentUser.setUpdatedAt(LocalDateTime.now());
|
||||
return userService.save(currentUser).map(ResponseEntity::ok);
|
||||
}
|
||||
|
||||
public record UpdateProfileRequest(String firstName, String lastName) {}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
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 org.springframework.data.relational.core.mapping.Table;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@Table(name = "organizations")
|
||||
@Table("organizations")
|
||||
public class Organization {
|
||||
@Id
|
||||
private UUID uuid;
|
||||
|
||||
@@ -2,9 +2,10 @@ package com.example.dateplanner.repositories;
|
||||
|
||||
import com.example.dateplanner.models.entities.Organization;
|
||||
import org.springframework.data.r2dbc.repository.R2dbcRepository;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public interface OrganizationRepository extends R2dbcRepository<Organization, UUID> {
|
||||
|
||||
Flux<Organization> findAllByOwnerUuid(UUID ownerUuid);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,27 @@
|
||||
package com.example.dateplanner.services;
|
||||
|
||||
import com.example.dateplanner.models.entities.Organization;
|
||||
import com.example.dateplanner.repositories.OrganizationRepository;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
@Service
|
||||
@AllArgsConstructor
|
||||
public class OrganizationService {
|
||||
private final OrganizationRepository organizationRepository;
|
||||
|
||||
public Flux<Organization> getByOwner(UUID ownerUuid) {
|
||||
return organizationRepository.findAllByOwnerUuid(ownerUuid);
|
||||
}
|
||||
|
||||
public Mono<Organization> create(Organization organization) {
|
||||
organization.setCreatedAt(LocalDateTime.now());
|
||||
organization.setUpdatedAt(LocalDateTime.now());
|
||||
return organizationRepository.save(organization);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.example.dateplanner.services;
|
||||
|
||||
import com.example.dateplanner.models.entities.AppUser;
|
||||
import com.example.dateplanner.repositories.AppUserRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -20,4 +21,8 @@ public class UserService implements ReactiveUserDetailsService {
|
||||
log.info("username {}", username);
|
||||
return userRepository.findByPhone(username).flatMap(Mono::just).cast(UserDetails.class);
|
||||
}
|
||||
|
||||
public Mono<AppUser> save(AppUser user) {
|
||||
return userRepository.save(user);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
/* ═══════════════════════════════════════════════════════════════
|
||||
LAYOUT — сайдбар и основной контент
|
||||
═══════════════════════════════════════════════════════════════ */
|
||||
|
||||
.sidebar {
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
padding: 25px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08);
|
||||
position: sticky;
|
||||
top: 30px;
|
||||
top: 80px; /* ниже fixed-header */
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
@@ -16,6 +20,27 @@
|
||||
min-height: 600px;
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════
|
||||
SIDEBAR — аватар, имя, прогресс
|
||||
═══════════════════════════════════════════════════════════════ */
|
||||
|
||||
/* Аватар-инициалы в сайдбаре */
|
||||
.profile-sidebar-avatar {
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #e74c3c 0%, #fd79a8 100%);
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
box-shadow: 0 8px 20px rgba(231, 76, 60, 0.3);
|
||||
border: 4px solid white;
|
||||
}
|
||||
|
||||
/* Устаревший аватар на основе фото (оставлен для совместимости) */
|
||||
.profile-user-avatar {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
@@ -26,8 +51,10 @@
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* Бейдж «Профиль на X%» */
|
||||
.match-badge {
|
||||
background: linear-gradient(45deg, #E74C3CFF, #FD79A8FF);
|
||||
display: inline-block;
|
||||
background: linear-gradient(45deg, #e74c3c, #fd79a8);
|
||||
color: white;
|
||||
padding: 5px 15px;
|
||||
border-radius: 20px;
|
||||
@@ -35,8 +62,9 @@
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Блок заполненности профиля */
|
||||
.profile-completion {
|
||||
background: #F9F7F7FF;
|
||||
background: #f9f7f7;
|
||||
border-radius: 15px;
|
||||
padding: 20px;
|
||||
margin: 20px 0;
|
||||
@@ -45,37 +73,280 @@
|
||||
.completion-percentage {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
color: #E74C3CFF;
|
||||
color: #e74c3c;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* Прогресс-бар */
|
||||
.progress {
|
||||
height: 10px !important;
|
||||
border-radius: 5px !important;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
background-color: #E74C3CFF !important;
|
||||
background-color: #e74c3c !important;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════
|
||||
SIDEBAR — навигация по разделам
|
||||
═══════════════════════════════════════════════════════════════ */
|
||||
|
||||
.profile-nav-link-custom {
|
||||
color: #2D3436FF;
|
||||
padding: 15px 20px;
|
||||
border-radius: 15px;
|
||||
margin-bottom: 10px;
|
||||
transition: all 0.3s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #2d3436;
|
||||
padding: 13px 18px;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 6px;
|
||||
transition: all 0.25s;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.profile-nav-link-custom:hover {
|
||||
background: rgba(231, 76, 60, 0.1) !important;
|
||||
color: #E74C3CFF !important;
|
||||
transform: translateX(5px) !important;
|
||||
background: rgba(231, 76, 60, 0.08);
|
||||
color: #e74c3c;
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
.profile-nav-link-custom.active {
|
||||
background: rgba(231, 76, 60, 0.1) !important;
|
||||
color: #E74C3CFF !important;
|
||||
border-left: 4px solid #E74C3CFF !important;
|
||||
background: rgba(231, 76, 60, 0.1);
|
||||
color: #e74c3c;
|
||||
border-left: 4px solid #e74c3c;
|
||||
padding-left: 14px;
|
||||
}
|
||||
|
||||
/* Счётчик уведомлений на пункте навигации */
|
||||
.notification-badge {
|
||||
margin-left: auto;
|
||||
background: #e74c3c;
|
||||
color: white;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 700;
|
||||
padding: 2px 7px;
|
||||
border-radius: 20px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════
|
||||
SECTION TITLE — заголовок внутри секции
|
||||
═══════════════════════════════════════════════════════════════ */
|
||||
|
||||
.section-title {
|
||||
border-bottom: 3px solid #f9f7f7;
|
||||
padding-bottom: 15px;
|
||||
margin-bottom: 30px;
|
||||
position: relative;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* Красная черта под заголовком */
|
||||
.section-title::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -3px;
|
||||
left: 0;
|
||||
width: 80px;
|
||||
height: 3px;
|
||||
background: #e74c3c;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════
|
||||
МОЙ ПРОФИЛЬ — карточка и поля
|
||||
═══════════════════════════════════════════════════════════════ */
|
||||
|
||||
/* Контейнер блока профиля */
|
||||
.profile-info-card {
|
||||
background: #f9f7f7;
|
||||
border-radius: 16px;
|
||||
padding: 28px;
|
||||
}
|
||||
|
||||
/* Большой аватар-инициалы в блоке профиля */
|
||||
.profile-big-avatar {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #e74c3c 0%, #fd79a8 100%);
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 26px;
|
||||
font-weight: 700;
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 5px 20px rgba(231, 76, 60, 0.3);
|
||||
}
|
||||
|
||||
/* Бейдж роли в блоке профиля */
|
||||
.profile-role-badge {
|
||||
display: inline-block;
|
||||
background: rgba(231, 76, 60, 0.1);
|
||||
color: #e74c3c;
|
||||
padding: 3px 14px;
|
||||
border-radius: 20px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* Сетка информационных полей */
|
||||
.profile-info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
/* Один элемент поля */
|
||||
.profile-info-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
/* Иконка поля */
|
||||
.profile-info-icon {
|
||||
color: #e74c3c;
|
||||
font-size: 17px;
|
||||
margin-top: 2px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Подпись поля */
|
||||
.profile-info-label {
|
||||
font-size: 0.75rem;
|
||||
color: #aaa;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
/* Значение поля */
|
||||
.profile-info-value {
|
||||
font-weight: 600;
|
||||
color: #2d3436;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════
|
||||
МОИ ОРГАНИЗАЦИИ — карточки и пустое состояние
|
||||
═══════════════════════════════════════════════════════════════ */
|
||||
|
||||
/* Карточка организации */
|
||||
.org-card {
|
||||
background: #f9f7f7;
|
||||
border-radius: 16px;
|
||||
padding: 20px;
|
||||
height: 100%;
|
||||
transition: box-shadow 0.2s, transform 0.2s;
|
||||
}
|
||||
|
||||
.org-card:hover {
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* Шапка карточки: лого + мета */
|
||||
.org-card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
/* Обёртка логотипа */
|
||||
.org-card-logo-wrap {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 14px;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
background: white;
|
||||
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Логотип-изображение */
|
||||
.org-card-logo-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
/* Логотип-инициалы (если нет картинки) */
|
||||
.org-card-logo-initials {
|
||||
background: linear-gradient(135deg, #e74c3c 0%, #fd79a8 100%);
|
||||
color: white;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* Название организации */
|
||||
.org-card-title {
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
color: #2d3436;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
/* Дата создания */
|
||||
.org-card-date {
|
||||
font-size: 0.8rem;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
/* Описание */
|
||||
.org-card-desc {
|
||||
font-size: 0.88rem;
|
||||
color: #636e72;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 0;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Пустое состояние (нет организаций) */
|
||||
.org-empty-state {
|
||||
text-align: center;
|
||||
padding: 60px 30px;
|
||||
background: #f9f7f7;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════
|
||||
МОДАЛКА СОЗДАНИЯ ОРГАНИЗАЦИИ — загрузчик логотипа
|
||||
═══════════════════════════════════════════════════════════════ */
|
||||
|
||||
/* Область загрузки логотипа */
|
||||
.org-logo-upload-area {
|
||||
width: 100%;
|
||||
aspect-ratio: 1;
|
||||
border: 2px dashed #ddd;
|
||||
border-radius: 14px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #f9f7f7;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.2s, background 0.2s;
|
||||
}
|
||||
|
||||
.org-logo-upload-area:hover {
|
||||
border-color: #e74c3c;
|
||||
background: rgba(231, 76, 60, 0.03);
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
function initHeader($header){
|
||||
console.log("init header date")
|
||||
|
||||
let authSection
|
||||
if(!auth) {
|
||||
authSection = `
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
// function initModalMap() {
|
||||
// let map = new ymaps3.map('map-container', {
|
||||
// center: [55.756806, 37.622727],
|
||||
// zoom: 11
|
||||
// })
|
||||
// }
|
||||
// await ymaps3.ready(initModalMap)
|
||||
//
|
||||
|
||||
initMap();
|
||||
|
||||
async function initMap() {
|
||||
// Промис `ymaps3.ready` будет зарезолвлен, когда загрузятся все компоненты основного модуля API
|
||||
await ymaps3.ready;
|
||||
|
||||
const {YMap, YMapDefaultSchemeLayer} = ymaps3;
|
||||
|
||||
// Иницилиазируем карту
|
||||
const map = new YMap(
|
||||
// Передаём ссылку на HTMLElement контейнера
|
||||
document.getElementById('map-container'),
|
||||
|
||||
// Передаём параметры инициализации карты
|
||||
{
|
||||
location: {
|
||||
// Координаты центра карты
|
||||
center: [37.588144, 55.733842],
|
||||
|
||||
// Уровень масштабирования
|
||||
zoom: 10
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Добавляем слой для отображения схематической карты
|
||||
map.addChild(new YMapDefaultSchemeLayer());
|
||||
}
|
||||
@@ -0,0 +1,607 @@
|
||||
function renderMyOrganizations($organizationsBlockContainer) {
|
||||
|
||||
// ── Модалка создания организации ──────────────────────────────────────────
|
||||
if (!$('#createOrganizationModal').length) {
|
||||
$('body').append(`
|
||||
<div class="modal fade" id="createOrganizationModal" data-bs-backdrop="static" tabindex="-1">
|
||||
<div class="modal-dialog modal-dialog-centered modal-lg">
|
||||
<div class="modal-content rounded-4 overflow-hidden border-0 shadow-lg">
|
||||
|
||||
<!-- Градиентная шапка -->
|
||||
<div class="modal-header border-0 p-4"
|
||||
style="background: linear-gradient(135deg, #e74c3c 0%, #fd79a8 100%)">
|
||||
<div class="text-white">
|
||||
<h4 class="fw-bold mb-1">
|
||||
<i class="fas fa-building me-2"></i>Новая организация
|
||||
</h4>
|
||||
<p class="mb-0 small" style="opacity: 0.8">
|
||||
Расскажите о вашей организации, чтобы привлечь больше пар
|
||||
</p>
|
||||
</div>
|
||||
<button type="button" class="btn-close btn-close-white ms-auto"
|
||||
data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body p-4">
|
||||
<form id="createOrganizationForm">
|
||||
<div class="row g-4">
|
||||
|
||||
<!-- Логотип -->
|
||||
<div class="col-md-4">
|
||||
<label class="form-label fw-bold">Логотип</label>
|
||||
<div class="org-logo-upload-area" id="orgLogoPreviewArea">
|
||||
<i class="fas fa-building" style="font-size:40px; color:#ddd"></i>
|
||||
<p class="text-muted small mt-2 mb-0">Нажмите для выбора</p>
|
||||
</div>
|
||||
<input type="file" class="form-control mt-2" id="orgLogoFile"
|
||||
accept="image/*" style="display:none">
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm w-100 mt-2"
|
||||
id="orgLogoPickBtn">
|
||||
<i class="fas fa-upload me-1"></i>Загрузить логотип
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Поля -->
|
||||
<div class="col-md-8">
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">
|
||||
Название организации <span class="text-danger">*</span>
|
||||
</label>
|
||||
<input type="text" class="form-control" id="orgTitle"
|
||||
placeholder="Например: Ресторан «Романтик»" required>
|
||||
<div class="form-text">Это имя будут видеть пользователи</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">Описание</label>
|
||||
<textarea class="form-control" id="orgDescription" rows="5"
|
||||
placeholder="Расскажите об атмосфере, уникальных предложениях, особенностях вашего заведения..."></textarea>
|
||||
<div class="form-text">Поддерживается HTML-разметка</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Информационный блок -->
|
||||
<div class="alert border mt-3 mb-0 d-flex align-items-center gap-3"
|
||||
style="background:#fff9f9; border-color:#f8d7da !important; border-radius:12px">
|
||||
<i class="fas fa-info-circle fs-5" style="color:#e74c3c; flex-shrink:0"></i>
|
||||
<div class="small text-muted">
|
||||
После создания организации вы сможете добавлять места для свиданий и
|
||||
управлять расписанием прямо из личного кабинета.
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer border-0 px-4 pb-4 pt-0">
|
||||
<button type="button" class="btn btn-outline-secondary rounded-pill px-4"
|
||||
data-bs-dismiss="modal">
|
||||
Отмена
|
||||
</button>
|
||||
<button type="button" class="btn btn-heart" id="saveOrganizationBtn">
|
||||
<i class="fas fa-check me-2"></i>Создать организацию
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
// ── Модалка создания места для свидания ───────────────────────────────────
|
||||
if (!$('#createDatingPlaceModal').length) {
|
||||
$('body').append(`
|
||||
<div class="modal fade" id="createDatingPlaceModal" data-bs-backdrop="static" tabindex="-1">
|
||||
<div class="modal-dialog modal-xl">
|
||||
<div class="modal-content rounded-4 border-0 shadow-lg">
|
||||
|
||||
<div class="modal-header border-0 p-4"
|
||||
style="background: linear-gradient(135deg, #9b59b6 0%, #3498db 100%)">
|
||||
<div class="text-white">
|
||||
<h4 class="fw-bold mb-1">
|
||||
<i class="fas fa-map-marker-alt me-2"></i>Новое место для свидания
|
||||
</h4>
|
||||
<p class="mb-0 small" style="opacity:0.8">
|
||||
Заполните информацию о вашем предложении
|
||||
</p>
|
||||
</div>
|
||||
<button type="button" class="btn-close btn-close-white ms-auto"
|
||||
data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body p-4">
|
||||
<form id="createDatingPlaceForm">
|
||||
|
||||
<!-- Основная информация -->
|
||||
<div class="card mb-4 border-0" style="background:#f9f7f7; border-radius:16px">
|
||||
<div class="card-body p-4">
|
||||
<h6 class="fw-bold mb-3 text-muted">
|
||||
<i class="bi bi-info-circle me-2" style="color:#e74c3c"></i>Основная информация
|
||||
</h6>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-8">
|
||||
<label class="form-label fw-bold">Название места <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" id="placeTitle"
|
||||
placeholder="Например: Романтический ужин на крыше" required>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label fw-bold">Тип свидания <span class="text-danger">*</span></label>
|
||||
<select class="form-select" id="datingType" required>
|
||||
<option value="" disabled selected>Выберите тип</option>
|
||||
<option value="ROMANTIC">💕 Романтическое</option>
|
||||
<option value="ACTIVE">⚡ Активное</option>
|
||||
<option value="INTELLECTUAL">📚 Интеллектуальное</option>
|
||||
<option value="HORROR">👻 Хоррор</option>
|
||||
<option value="COZY">🕯️ Уют</option>
|
||||
<option value="MYSTERY">🔮 Тайна</option>
|
||||
<option value="CREATIVE">🎨 Творческое</option>
|
||||
<option value="OPENAIR">🌳 На открытом воздухе</option>
|
||||
<option value="GASTRONOMIC">🍷 Гастрономия</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label fw-bold">Описание <span class="text-danger">*</span></label>
|
||||
<textarea class="form-control" id="placeDescription" rows="4"
|
||||
placeholder="Подробно опишите, что ждет пару..." required></textarea>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label fw-bold">Фото</label>
|
||||
<input type="file" class="form-control" id="placePhoto" accept="image/*">
|
||||
<div id="photoPreview" class="mt-2 d-none">
|
||||
<img src="" alt="Preview" class="rounded-3"
|
||||
style="max-height:150px; object-fit:cover">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Местоположение -->
|
||||
<div class="card mb-4 border-0" style="background:#f9f7f7; border-radius:16px">
|
||||
<div class="card-body p-4">
|
||||
<h6 class="fw-bold mb-3 text-muted">
|
||||
<i class="bi bi-geo-alt me-2" style="color:#e74c3c"></i>Местоположение
|
||||
</h6>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6 p-1" id="map-container" style="height:50vh; border-radius:12px; overflow:hidden"></div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">Адрес <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" id="placeLocation"
|
||||
placeholder="ул. Пушкина, д. 10" required>
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label fw-bold">Координаты</label>
|
||||
<input type="text" class="form-control" id="placeCoordinates"
|
||||
placeholder="55.7558, 37.6173">
|
||||
<div class="form-text">Широта, долгота (можно выбрать на карте)</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Цена -->
|
||||
<div class="card mb-4 border-0" style="background:#f9f7f7; border-radius:16px">
|
||||
<div class="card-body p-4">
|
||||
<h6 class="fw-bold mb-3 text-muted">
|
||||
<i class="bi bi-currency-ruble me-2" style="color:#e74c3c"></i>Стоимость
|
||||
</h6>
|
||||
<div class="row g-3 align-items-end">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label fw-bold">Точная цена (₽)</label>
|
||||
<div class="input-group">
|
||||
<input type="number" class="form-control" id="exactPrice"
|
||||
placeholder="5000" min="0" step="100">
|
||||
<span class="input-group-text">₽</span>
|
||||
</div>
|
||||
<div class="form-text">Категория определится автоматически</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label fw-bold">Или ценовая категория</label>
|
||||
<select class="form-select" id="priceType">
|
||||
<option value="" selected>Не выбрано</option>
|
||||
<option value="ECONOMY">💰 Эконом (до 2000₽)</option>
|
||||
<option value="AVERAGE">💵 Средний (2000–5000₽)</option>
|
||||
<option value="PREMIUM">💎 Премиум (5000+₽)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="form-check mb-2">
|
||||
<input class="form-check-input" type="checkbox" id="freeEvent">
|
||||
<label class="form-check-label" for="freeEvent">Бесплатное мероприятие</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="priceInfo" class="alert alert-info mt-3 d-none py-2">
|
||||
<i class="bi bi-info-circle me-1"></i>
|
||||
Категория: <strong id="selectedPriceCategory"></strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Расписание -->
|
||||
<div class="card mb-4 border-0" style="background:#f9f7f7; border-radius:16px">
|
||||
<div class="card-body p-4">
|
||||
<h6 class="fw-bold mb-3 text-muted">
|
||||
<i class="bi bi-clock me-2" style="color:#e74c3c"></i>Время проведения
|
||||
</h6>
|
||||
<div class="mb-3 d-flex gap-4">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="scheduleType"
|
||||
id="scheduleOneTime" value="onetime" checked>
|
||||
<label class="form-check-label" for="scheduleOneTime">Разовое событие</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="scheduleType"
|
||||
id="scheduleRegular" value="regular">
|
||||
<label class="form-check-label" for="scheduleRegular">Регулярное</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="oneTimeSchedule">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-bold">Дата и время начала</label>
|
||||
<input type="datetime-local" class="form-control" id="eventStartTime">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-bold">Длительность (минут)</label>
|
||||
<input type="number" class="form-control" id="eventDuration"
|
||||
placeholder="120" min="15" step="15">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-bold">Дата окончания актуальности</label>
|
||||
<input type="date" class="form-control" id="expirationDate">
|
||||
<div class="form-text">Если это временное предложение</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="regularSchedule" class="d-none">
|
||||
<div id="scheduleItems"></div>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm mt-2"
|
||||
id="addScheduleItem">
|
||||
<i class="bi bi-plus-circle me-1"></i>Добавить день
|
||||
</button>
|
||||
<template id="scheduleItemTemplate">
|
||||
<div class="row g-2 mb-2 schedule-item align-items-center">
|
||||
<div class="col-md-5">
|
||||
<select class="form-select">
|
||||
<option value="MONDAY">Понедельник</option>
|
||||
<option value="TUESDAY">Вторник</option>
|
||||
<option value="WEDNESDAY">Среда</option>
|
||||
<option value="THURSDAY">Четверг</option>
|
||||
<option value="FRIDAY">Пятница</option>
|
||||
<option value="SATURDAY">Суббота</option>
|
||||
<option value="SUNDAY">Воскресенье</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<input type="time" class="form-control" placeholder="Начало">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<input type="time" class="form-control" placeholder="Конец">
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
<button type="button" class="btn btn-outline-danger btn-sm remove-schedule">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer border-0 px-4 pb-4 pt-0">
|
||||
<button type="button" class="btn btn-outline-secondary rounded-pill px-4"
|
||||
data-bs-dismiss="modal">Отмена</button>
|
||||
<button type="submit" form="createDatingPlaceForm" class="btn btn-heart">
|
||||
<i class="fas fa-check me-2"></i>Создать место
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
// ── Секция "Мои организации" ──────────────────────────────────────────────
|
||||
$organizationsBlockContainer.append(`
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2 class="section-title mb-0">Мои организации</h2>
|
||||
<button type="button" class="btn btn-heart"
|
||||
data-bs-toggle="modal" data-bs-target="#createOrganizationModal">
|
||||
<i class="fas fa-plus me-2"></i>Создать организацию
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-muted mb-4">
|
||||
Управляйте своими организациями — добавляйте места и мероприятия для свиданий
|
||||
</p>
|
||||
<div id="orgCardsList" class="row g-4">
|
||||
<div class="col-12 text-center py-5 text-muted" id="orgLoadingSpinner">
|
||||
<div class="spinner-border" style="color:#e74c3c" role="status"></div>
|
||||
<p class="mt-2">Загружаем организации...</p>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
|
||||
// ── Загрузка организаций ──────────────────────────────────────────────────
|
||||
function loadOrganizations() {
|
||||
fetch('/api/organizations/my')
|
||||
.then(r => r.json())
|
||||
.then(orgs => {
|
||||
const $list = $('#orgCardsList');
|
||||
$list.empty();
|
||||
|
||||
if (!orgs.length) {
|
||||
$list.append(`
|
||||
<div class="col-12">
|
||||
<div class="org-empty-state">
|
||||
<i class="fas fa-building" style="font-size:48px; color:#ddd"></i>
|
||||
<h5 class="mt-3 mb-2">У вас пока нет организаций</h5>
|
||||
<p class="text-muted mb-4">
|
||||
Создайте первую организацию и начните добавлять места для свиданий
|
||||
</p>
|
||||
<button class="btn btn-heart"
|
||||
data-bs-toggle="modal" data-bs-target="#createOrganizationModal">
|
||||
<i class="fas fa-plus me-2"></i>Создать первую организацию
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
return;
|
||||
}
|
||||
|
||||
orgs.forEach(org => {
|
||||
const initials = org.title
|
||||
? org.title.substring(0, 2).toUpperCase()
|
||||
: 'ОГ';
|
||||
const logo = org.logo
|
||||
? `<img src="${org.logo}" alt="${org.title}" class="org-card-logo-img">`
|
||||
: `<div class="org-card-logo-initials">${initials}</div>`;
|
||||
const desc = org.description
|
||||
? org.description.replace(/<[^>]*>/g, '').substring(0, 100) + '...'
|
||||
: 'Описание не добавлено';
|
||||
const date = org.createdAt
|
||||
? new Date(org.createdAt).toLocaleDateString('ru-RU')
|
||||
: '';
|
||||
|
||||
$list.append(`
|
||||
<div class="col-md-6">
|
||||
<div class="org-card">
|
||||
<div class="org-card-header">
|
||||
<div class="org-card-logo-wrap">${logo}</div>
|
||||
<div class="org-card-meta">
|
||||
<h5 class="org-card-title">${org.title}</h5>
|
||||
${date ? `<span class="org-card-date"><i class="fas fa-calendar-alt me-1"></i>${date}</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
<p class="org-card-desc">${desc}</p>
|
||||
<div class="d-flex gap-2 mt-3">
|
||||
<button class="btn btn-heart btn-sm rounded-pill px-3"
|
||||
data-bs-toggle="modal" data-bs-target="#createDatingPlaceModal">
|
||||
<i class="fas fa-plus me-1"></i>Добавить место
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary btn-sm rounded-pill px-3">
|
||||
<i class="fas fa-edit me-1"></i>Редактировать
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Ошибка загрузки организаций:', err);
|
||||
$('#orgCardsList').html(`
|
||||
<div class="col-12 text-center py-4 text-muted">
|
||||
<i class="fas fa-exclamation-triangle text-warning fs-3"></i>
|
||||
<p class="mt-2">Не удалось загрузить организации</p>
|
||||
</div>
|
||||
`);
|
||||
});
|
||||
}
|
||||
|
||||
// ── Обработчики модалки создания организации ──────────────────────────────
|
||||
|
||||
// Превью логотипа
|
||||
$(document).on('change', '#orgLogoFile', function () {
|
||||
const file = this.files[0];
|
||||
if (!file) return;
|
||||
const reader = new FileReader();
|
||||
reader.onload = e => {
|
||||
$('#orgLogoPreviewArea').html(`
|
||||
<img src="${e.target.result}" alt="Logo" style="width:100%;height:100%;object-fit:cover">
|
||||
`);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
|
||||
$(document).on('click', '#orgLogoPickBtn', () => $('#orgLogoFile').trigger('click'));
|
||||
$(document).on('click', '#orgLogoPreviewArea', () => $('#orgLogoFile').trigger('click'));
|
||||
|
||||
// Создание организации
|
||||
$(document).on('click', '#saveOrganizationBtn', async function () {
|
||||
const title = $('#orgTitle').val().trim();
|
||||
if (!title) {
|
||||
$('#orgTitle').addClass('is-invalid').focus();
|
||||
return;
|
||||
}
|
||||
$('#orgTitle').removeClass('is-invalid');
|
||||
|
||||
const $btn = $(this).prop('disabled', true)
|
||||
.html('<span class="spinner-border spinner-border-sm me-2"></span>Создаём...');
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/organizations', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-XSRF-TOKEN': csrf?.token ?? ''
|
||||
},
|
||||
body: JSON.stringify({
|
||||
title,
|
||||
description: $('#orgDescription').val().trim() || null
|
||||
})
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
bootstrap.Modal.getInstance(document.getElementById('createOrganizationModal')).hide();
|
||||
$('#createOrganizationForm')[0].reset();
|
||||
$('#orgLogoPreviewArea').html(`
|
||||
<i class="fas fa-building" style="font-size:40px; color:#ddd"></i>
|
||||
<p class="text-muted small mt-2 mb-0">Нажмите для выбора</p>
|
||||
`);
|
||||
loadOrganizations();
|
||||
showToast('info', `Организация «${title}» успешно создана`);
|
||||
} else {
|
||||
showToast('error', 'Не удалось создать организацию');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Org create error:', err);
|
||||
showToast('error', 'Ошибка соединения с сервером');
|
||||
} finally {
|
||||
$btn.prop('disabled', false).html('<i class="fas fa-check me-2"></i>Создать организацию');
|
||||
}
|
||||
});
|
||||
|
||||
// ── Обработчики модалки создания места ───────────────────────────────────
|
||||
|
||||
// Переключение расписания
|
||||
$(document).on('change', 'input[name="scheduleType"]', function () {
|
||||
if (this.value === 'onetime') {
|
||||
$('#oneTimeSchedule').removeClass('d-none');
|
||||
$('#regularSchedule').addClass('d-none');
|
||||
} else {
|
||||
$('#oneTimeSchedule').addClass('d-none');
|
||||
$('#regularSchedule').removeClass('d-none');
|
||||
}
|
||||
});
|
||||
|
||||
// Добавление дня расписания
|
||||
let scheduleItemIndex = 0;
|
||||
$(document).on('click', '#addScheduleItem', function () {
|
||||
const template = document.getElementById('scheduleItemTemplate');
|
||||
const clone = template.content.cloneNode(true);
|
||||
const item = clone.querySelector('.schedule-item');
|
||||
item.dataset.index = scheduleItemIndex++;
|
||||
item.querySelector('.remove-schedule').addEventListener('click', function () {
|
||||
this.closest('.schedule-item').remove();
|
||||
});
|
||||
document.getElementById('scheduleItems').appendChild(clone);
|
||||
});
|
||||
|
||||
// Превью фото места
|
||||
$(document).on('change', '#placePhoto', function () {
|
||||
const file = this.files[0];
|
||||
if (!file) return;
|
||||
const reader = new FileReader();
|
||||
reader.onload = e => {
|
||||
$('#photoPreview').find('img').attr('src', e.target.result);
|
||||
$('#photoPreview').removeClass('d-none');
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
|
||||
// Авто-категория по цене
|
||||
$(document).on('input', '#exactPrice', function () {
|
||||
const price = parseInt(this.value);
|
||||
if (isNaN(price) || price <= 0) {
|
||||
$('#priceInfo').addClass('d-none');
|
||||
return;
|
||||
}
|
||||
let cat, val;
|
||||
if (price < 2000) { cat = 'Эконом'; val = 'ECONOMY'; }
|
||||
else if (price <= 5000){ cat = 'Средний'; val = 'AVERAGE'; }
|
||||
else { cat = 'Премиум'; val = 'PREMIUM'; }
|
||||
$('#priceType').val(val);
|
||||
$('#selectedPriceCategory').text(`${cat} (${price}₽)`);
|
||||
$('#priceInfo').removeClass('d-none');
|
||||
});
|
||||
|
||||
// Бесплатное событие
|
||||
$(document).on('change', '#freeEvent', function () {
|
||||
if (this.checked) {
|
||||
$('#exactPrice').val(0).prop('disabled', true);
|
||||
$('#priceType').val('ECONOMY');
|
||||
$('#selectedPriceCategory').text('Бесплатно');
|
||||
$('#priceInfo').removeClass('d-none');
|
||||
} else {
|
||||
$('#exactPrice').val('').prop('disabled', false);
|
||||
$('#priceType').val('');
|
||||
$('#priceInfo').addClass('d-none');
|
||||
}
|
||||
});
|
||||
|
||||
// Отправка формы создания места
|
||||
$(document).on('submit', '#createDatingPlaceForm', async function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
let price = 0, priceType = null;
|
||||
if ($('#freeEvent').is(':checked')) {
|
||||
priceType = 'ECONOMY';
|
||||
} else {
|
||||
const exactPrice = $('#exactPrice').val();
|
||||
if (exactPrice) {
|
||||
price = parseInt(exactPrice);
|
||||
if (price < 2000) priceType = 'ECONOMY';
|
||||
else if (price <= 5000) priceType = 'AVERAGE';
|
||||
else priceType = 'PREMIUM';
|
||||
} else {
|
||||
priceType = $('#priceType').val();
|
||||
}
|
||||
}
|
||||
|
||||
const scheduleType = $('input[name="scheduleType"]:checked').val();
|
||||
const formData = {
|
||||
title: $('#placeTitle').val(),
|
||||
description: $('#placeDescription').val(),
|
||||
location: $('#placeLocation').val(),
|
||||
coordinates: $('#placeCoordinates').val() || null,
|
||||
datingType: $('#datingType').val(),
|
||||
price, priceType,
|
||||
startTime: null,
|
||||
duration: null,
|
||||
scheduleUuids: [],
|
||||
expirationDate: null
|
||||
};
|
||||
|
||||
if (scheduleType === 'onetime') {
|
||||
const st = $('#eventStartTime').val();
|
||||
if (st) formData.startTime = new Date(st).toISOString();
|
||||
formData.duration = parseInt($('#eventDuration').val()) || null;
|
||||
const exp = $('#expirationDate').val();
|
||||
if (exp) formData.expirationDate = new Date(exp).toISOString();
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/dating-places', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-XSRF-TOKEN': csrf?.token ?? ''
|
||||
},
|
||||
body: JSON.stringify(formData)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
bootstrap.Modal.getInstance(document.getElementById('createDatingPlaceModal')).hide();
|
||||
showToast('info', 'Место успешно создано!');
|
||||
} else {
|
||||
showToast('error', 'Ошибка при создании места');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Dating place create error:', err);
|
||||
showToast('error', 'Ошибка соединения с сервером');
|
||||
}
|
||||
});
|
||||
|
||||
// Инициализация
|
||||
loadOrganizations();
|
||||
}
|
||||
|
||||
renderMyOrganizations($('#organizationsSection'));
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,85 +1,86 @@
|
||||
<div class="container py-5" id="mainPageContainer">
|
||||
<div class="row">
|
||||
<div class="col-lg-4 mb-4">
|
||||
<!-- Левая панель: аватар + навигация -->
|
||||
<div class="col-lg-4 mb-4" id="left-panel">
|
||||
<div class="sidebar">
|
||||
<div class="text-center">
|
||||
<img src="https://images.unsplash.com/photo-1556909114-f6e7ad7d3136?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80"
|
||||
class="profile-user-avatar" alt="Анна">
|
||||
<h4 class="mb-1">Анна, 26</h4>
|
||||
<p class="text-muted mb-3">Москва</p>
|
||||
<div class="match-badge">Профиль на 85%</div>
|
||||
<div class="profile-sidebar-avatar mx-auto mb-3" id="sidebarAvatarInitials"></div>
|
||||
<h4 class="mb-1 fw-bold" id="sidebarFullName"></h4>
|
||||
<p class="text-muted mb-3" id="sidebarPhone"></p>
|
||||
<div class="match-badge" id="sidebarRoleBadge"></div>
|
||||
</div>
|
||||
|
||||
<div class="profile-completion">
|
||||
<div class="d-flex justify-content-between mb-2">
|
||||
<span>Заполненность профиля:</span>
|
||||
<span class="completion-percentage">85%</span>
|
||||
</div>
|
||||
<div class="progress">
|
||||
<div class="progress-bar" style="width: 85%"></div>
|
||||
</div>
|
||||
<p class="small text-muted mt-2 mb-0">Полный профиль привлекает на 60% больше откликов</p>
|
||||
</div>
|
||||
|
||||
<nav class="nav flex-column" id="profile-sections-navigator">
|
||||
<a class="profile-nav-link-custom active" data-section="profileSection" href="#"
|
||||
onclick="showSection('profile')">
|
||||
<nav class="nav flex-column mt-4" id="profile-sections-navigator">
|
||||
<a class="profile-nav-link-custom active" data-section="profileSection" href="#">
|
||||
<i class="fas fa-user me-3"></i>Мой профиль
|
||||
</a>
|
||||
<a class="profile-nav-link-custom" data-section="proposalsSection" href="#"
|
||||
onclick="showSection('proposals')">
|
||||
<a class="profile-nav-link-custom" data-section="proposalsSection" href="#">
|
||||
<i class="fas fa-heart me-3"></i>Мои предложения
|
||||
<span class="notification-badge">3</span>
|
||||
</a>
|
||||
<a class="profile-nav-link-custom" data-section="organizationsSection" href="#"
|
||||
onclick="showSection('organizations')">
|
||||
<a class="profile-nav-link-custom" data-section="organizationsSection" href="#">
|
||||
<i class="fas fa-building me-3"></i>Мои организации
|
||||
</a>
|
||||
<a class="profile-nav-link-custom" data-section="eventsSection" href="#"
|
||||
onclick="showSection('events')">
|
||||
<a class="profile-nav-link-custom" data-section="eventsSection" href="#">
|
||||
<i class="fas fa-calendar-alt me-3"></i>Мероприятия
|
||||
</a>
|
||||
<a class="profile-nav-link-custom" data-section="settingsSection" href="#"
|
||||
onclick="showSection('settings')">
|
||||
<a class="profile-nav-link-custom" data-section="settingsSection" href="#">
|
||||
<i class="fas fa-cog me-3"></i>Настройки
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
<div class="mt-4">
|
||||
<button class="btn btn-heart w-100" onclick="createNewProposal()">
|
||||
<button class="btn btn-heart w-100">
|
||||
<i class="fas fa-plus me-2"></i>Предложить свидание
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Правая панель: контент секций -->
|
||||
<div class="col-lg-8">
|
||||
<div class="profile-main-content">
|
||||
<div class="section-content d-block" id="profileSection">
|
||||
1
|
||||
</div>
|
||||
<div class="section-content d-block" id="profileSection"></div>
|
||||
<div class="section-content d-none" id="proposalsSection">
|
||||
2
|
||||
</div>
|
||||
<div class="section-content d-none" id="organizationsSection">
|
||||
3
|
||||
<h2 class="section-title">Мои предложения</h2>
|
||||
<p class="text-muted">Раздел в разработке</p>
|
||||
</div>
|
||||
<div class="section-content d-none" id="organizationsSection"
|
||||
style="max-height: 2000px; overflow-x: auto;"></div>
|
||||
<div class="section-content d-none" id="eventsSection">
|
||||
4
|
||||
<h2 class="section-title">Мероприятия</h2>
|
||||
<p class="text-muted">Раздел в разработке</p>
|
||||
</div>
|
||||
<div class="section-content d-none" id="settingsSection">
|
||||
5
|
||||
<h2 class="section-title">Настройки</h2>
|
||||
<p class="text-muted">Раздел в разработке</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$('#profile-sections-navigator').on('click', '.profile-nav-link-custom', function () {
|
||||
$('#profile-sections-navigator').find('.profile-nav-link-custom').removeClass('active');
|
||||
// Навигация по секциям
|
||||
$('#profile-sections-navigator').on('click', '.profile-nav-link-custom', function (e) {
|
||||
e.preventDefault();
|
||||
$('#profile-sections-navigator .profile-nav-link-custom').removeClass('active');
|
||||
$(this).addClass('active');
|
||||
const selectedSectionId = $(this).data('section');
|
||||
console.log(selectedSectionId);
|
||||
const sectionId = $(this).data('section');
|
||||
$('.section-content').removeClass('d-block').addClass('d-none');
|
||||
$(`#${selectedSectionId}`).removeClass('d-none').addClass('d-block');
|
||||
})
|
||||
$(`#${sectionId}`).removeClass('d-none').addClass('d-block');
|
||||
});
|
||||
|
||||
// Заполняем сайдбар данными из переменной user (инжектированной сервером)
|
||||
if (user) {
|
||||
const initials = `${user.firstName.charAt(0)}${user.lastName.charAt(0)}`;
|
||||
$('#sidebarAvatarInitials').text(initials);
|
||||
$('#sidebarFullName').text(`${user.firstName} ${user.lastName}`);
|
||||
$('#sidebarPhone').text(user.phone);
|
||||
$('#sidebarRoleBadge').text(user.role || 'USER');
|
||||
}
|
||||
</script>
|
||||
|
||||
<script src="/js/site/pages/account/my-profile-block.js"></script>
|
||||
<script src="/js/site/pages/account/my-organizations-block.js"></script>
|
||||
<script src="/js/site/pages/account/init-modal-map.js"></script>
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
<link href="/css/style.css" rel="stylesheet">
|
||||
<link href="/js/fw/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="/js/fw/bootstrap-icons-1.11.3/font/bootstrap-icons.css" rel="stylesheet">
|
||||
<script src="https://api-maps.yandex.ru/v3/?apikey=7c13cf7d-c937-4ca6-8021-145ac16bdcae&lang=ru_RU"></script>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<script src="/js/fw/jquery/dist/jquery.js"></script>
|
||||
<script src="/js/fw/apexcharts/loader.js"></script>
|
||||
|
||||
Reference in New Issue
Block a user