Files
tourrism_site/views/guides/index-backup.ejs
Andrey K. Choi a461fea9d9 Компактные hero секции и улучшенная инициализация БД
🎨 UI улучшения:
- Уменьшена высота синих панелей с 100vh до 70vh на главной
- Добавлен класс .compact (25vh) для всех остальных страниц
- Улучшена адаптивность для мобильных устройств
- Обновлены все шаблоны с hero секциями

🚀 Инфраструктура:
- Автоматическая инициализация базы данных при деплое
- Улучшены мокапные данные (больше отзывов, бронирований, сообщений)
- Добавлены настройки сайта в базу данных
- Создан скрипт автоматического деплоя deploy.sh

📦 Система сборки:
- Обновлен .gitignore с полным покрытием файлов
- Добавлена папка для загрузок с .gitkeep
- Улучшен README с инструкциями по запуску
- ES модули для инициализации базы данных

🐛 Исправления:
- Совместимость с ES модулями в Node.js
- Правильная обработка ошибок инициализации БД
- Корректные SQL запросы для PostgreSQL
2025-11-29 18:47:42 +09:00

316 lines
15 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!-- Hero Section -->
<section class="hero-section compact bg-primary text-white text-center py-5">
<div class="container">
<div class="row">
<div class="col-lg-8 mx-auto">
<h1 class="display-4 fw-bold mb-4">Наши гиды</h1>
<p class="lead">Профессиональные и опытные гиды сделают ваше путешествие незабываемым</p>
</div>
</div>
</div>
</section>
<!-- Guides Grid -->
<section class="py-5">
<div class="container">
<% if (guides.length === 0) { %>
<div class="text-center py-5">
<i class="fas fa-user-tie text-muted" style="font-size: 4rem;"></i>
<h3 class="mt-3 text-muted">Гиды не найдены</h3>
<p class="text-muted">В данный момент нет доступных гидов.</p>
</div>
<% } else { %>
<div class="row">
<% guides.forEach(guide => { %>
<div class="col-lg-4 col-md-6 mb-4">
<div class="card h-100 shadow-sm guide-card" data-guide-id="<%= guide.id %>" style="cursor: pointer; transition: transform 0.2s;">
<% if (guide.image_url && guide.image_url.trim()) { %>
<img src="<%= guide.image_url %>" class="card-img-top" alt="<%= guide.name %>" style="height: 250px; object-fit: cover;">
<% } else { %>
<img src="/images/placeholders/default-guide.svg" class="card-img-top" alt="<%= guide.name %>" style="height: 250px; object-fit: cover;">
<% } %>
<div class="card-body d-flex flex-column">
<div class="d-flex justify-content-between align-items-start mb-2">
<h5 class="card-title mb-0"><%= guide.name %></h5>
<div class="rating-display" data-target-type="guide" data-target-id="<%= guide.id %>">
<div class="d-flex align-items-center">
<span class="rating-percentage text-warning fw-bold">
<% if (guide.rating_percentage > 0) { %>
<%= parseFloat(guide.rating_percentage).toFixed(0) %>%
<% } else { %>
0%
<% } %>
</span>
<small class="text-muted ms-1">
(<%= guide.likes || 0 %>/<%= (parseInt(guide.likes || 0) + parseInt(guide.dislikes || 0)) %>)
</small>
</div>
<div class="rating-buttons mt-1">
<button class="btn btn-sm btn-outline-success like-btn" data-rating="1">
<i class="fas fa-thumbs-up"></i> <span class="like-count"><%= guide.likes || 0 %></span>
</button>
<button class="btn btn-sm btn-outline-danger dislike-btn ms-1" data-rating="-1">
<i class="fas fa-thumbs-down"></i> <span class="dislike-count"><%= guide.dislikes || 0 %></span>
</button>
</div>
</div>
</div>
<div class="mb-3">
<% if (guide.specialization === 'city') { %>
<span class="badge bg-info">
<i class="fas fa-city me-1"></i>Городские туры
</span>
<% } else if (guide.specialization === 'mountain') { %>
<span class="badge bg-success">
<i class="fas fa-mountain me-1"></i>Горные походы
</span>
<% } else if (guide.specialization === 'fishing') { %>
<span class="badge bg-primary">
<i class="fas fa-fish me-1"></i>Рыбалка
</span>
<% } else { %>
<span class="badge bg-secondary">
<i class="fas fa-star me-1"></i>Универсальный
</span>
<% } %>
<% if (guide.experience > 0) { %>
<span class="badge bg-warning text-dark ms-1">
<i class="fas fa-medal me-1"></i><%= guide.experience %> лет опыта
</span>
<% } %>
</div>
<% if (guide.bio) { %>
<p class="card-text text-muted flex-grow-1"><%= guide.bio.length > 100 ? guide.bio.substring(0, 100) + '...' : guide.bio %></p>
<% } %>
<div class="mt-auto">
<% if (guide.languages) { %>
<div class="mb-2">
<small class="text-muted">
<i class="fas fa-language me-1"></i>
<%= guide.languages %>
</small>
</div>
<% } %>
<% if (guide.avg_rating) { %>
<div class="mb-2">
<i class="fas fa-star text-warning"></i>
<i class="fas fa-star text-warning"></i>
<i class="fas fa-star text-warning"></i>
<i class="fas fa-star text-warning"></i>
<i class="far fa-star text-muted"></i>
<small class="text-muted ms-2">(<%= parseFloat(guide.avg_rating).toFixed(1) %>)</small>
</div>
<% } %>
<div class="d-flex justify-content-between align-items-center">
<div class="text-muted small">
<i class="fas fa-calendar-check me-1"></i>
<%= guide.booking_count || 0 %> туров проведено
</div>
<% if (guide.hourly_rate) { %>
<div class="fw-bold text-primary">
₩<%= parseInt(guide.hourly_rate || 0).toLocaleString('ko-KR') %>/час
</div>
<% } %>
</div>
</div>
</div>
</div>
</div>
<% }); %>
</div>
<% } %>
</div>
</section>
<!-- Guide Detail Modal -->
<div class="modal fade" id="guideModal" tabindex="-1" aria-labelledby="guideModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="guideModalLabel">Информация о гиде</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body" id="guideModalBody">
<!-- Динамически загружаемая информация о гиде -->
<div class="text-center">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Загрузка...</span>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Закрыть</button>
<button type="button" class="btn btn-primary" id="bookGuideBtn">
<i class="fas fa-calendar-plus me-2"></i>Забронировать
</button>
</div>
</div>
</div>
</div>
<style>
.guide-card:hover {
transform: translateY(-5px);
box-shadow: 0 .5rem 1rem rgba(0,0,0,.15)!important;
}
.rating-buttons .btn {
font-size: 0.75rem;
padding: 0.25rem 0.5rem;
}
.rating-percentage {
font-size: 1.1rem;
}
.rating-buttons .btn.active {
opacity: 1;
}
.rating-buttons .btn:not(.active) {
opacity: 0.6;
}
</style>
<script>
$(document).ready(function() {
// Обработчик клика по карточке гида
$('.guide-card').on('click', function(e) {
// Не открываем модальное окно при клике на кнопки рейтинга
if ($(e.target).closest('.rating-buttons').length > 0) {
return;
}
const guideId = $(this).data('guide-id');
loadGuideDetails(guideId);
$('#guideModal').modal('show');
});
// Обработчики для кнопок лайк/дизлайк
$('.like-btn, .dislike-btn').on('click', function(e) {
e.stopPropagation();
const $container = $(this).closest('.rating-display');
const targetType = $container.data('target-type');
const targetId = $container.data('target-id');
const rating = parseInt($(this).data('rating'));
setRating(targetType, targetId, rating, $container);
});
});
function loadGuideDetails(guideId) {
$('#guideModalBody').html(`
<div class="text-center">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Загрузка...</span>
</div>
</div>
`);
$.get(`/guides/${guideId}`)
.done(function(html) {
$('#guideModalBody').html(html);
$('#bookGuideBtn').data('guide-id', guideId);
})
.fail(function() {
$('#guideModalBody').html(`
<div class="text-center text-danger">
<i class="fas fa-exclamation-triangle fa-2x mb-3"></i>
<p>Ошибка загрузки информации о гиде</p>
</div>
`);
});
}
function setRating(targetType, targetId, rating, $container) {
$.post(`/api/rating/${targetType}/${targetId}`, { rating: rating })
.done(function(data) {
if (data.success) {
// Обновляем отображение рейтинга
updateRatingDisplay($container, data.rating);
// Показываем уведомление
showToast(data.message, 'success');
}
})
.fail(function(xhr) {
const error = xhr.responseJSON?.error || 'Ошибка при установке рейтинга';
showToast(error, 'error');
});
}
function updateRatingDisplay($container, ratingData) {
const percentage = parseFloat(ratingData.rating_percentage || 0);
const likes = parseInt(ratingData.likes_count || 0);
const dislikes = parseInt(ratingData.dislikes_count || 0);
const total = likes + dislikes;
$container.find('.rating-percentage').text(percentage.toFixed(0) + '%');
$container.find('.like-count').text(likes);
$container.find('.dislike-count').text(dislikes);
$container.find('small.text-muted').text(`(${likes}/${total})`);
}
function showToast(message, type) {
const bgClass = type === 'success' ? 'bg-success' : 'bg-danger';
const toast = $(`
<div class="toast align-items-center text-white ${bgClass} border-0" role="alert"
style="position: fixed; top: 20px; right: 20px; z-index: 9999;">
<div class="d-flex">
<div class="toast-body">${message}</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
</div>
</div>
`);
$('body').append(toast);
new bootstrap.Toast(toast[0]).show();
// Удаляем toast после скрытия
toast.on('hidden.bs.toast', function() {
$(this).remove();
});
}
</script>
<small class="text-muted">
<i class="fas fa-calendar me-1"></i><%= guide.experience %> лет опыта
</small>
</div>
<div>
<small class="text-muted">
<i class="fas fa-route me-1"></i><%= guide.route_count || 0 %> туров
</small>
</div>
</div>
<a href="/guides/<%= guide.id %>" class="btn btn-primary w-100">
<i class="fas fa-user me-1"></i>Подробнее
</a>
</div>
</div>
</div>
</div>
<% }); %>
</div>
<% } %>
</div>
</section>
<!-- CTA Section -->
<section class="bg-light py-5">
<div class="container text-center">
<h2 class="mb-4">Нужен индивидуальный подход?</h2>
<p class="lead text-muted mb-4">Наши гиды готовы создать уникальный маршрут специально для вас!</p>
<a href="/contact" class="btn btn-primary btn-lg">
<i class="fas fa-envelope me-1"></i>Связаться с нами
</a>
</div>
</section>