- Объединены ресурсы в 5 логических групп: Контент сайта, Бронирования, Отзывы и рейтинги, Персонал и гиды, Администрирование - Удалены дублирующие настройки navigation для чистой группировки - Добавлены CSS стили для визуального отображения иерархии с отступами - Добавлены эмодзи-иконки для каждого типа ресурсов через CSS - Улучшена навигация с правильной вложенностью элементов
219 lines
11 KiB
Plaintext
219 lines
11 KiB
Plaintext
<!-- Hero Section -->
|
||
<section class="hero-section compact bg-gradient-primary text-white py-5">
|
||
<div class="container text-center">
|
||
<h1 class="display-4 fw-bold mb-3">Наши профессиональные гиды</h1>
|
||
<p class="lead">Опытные гиды для незабываемых путешествий по Корее</p>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- Guide Availability Calendar -->
|
||
<section class="py-4 bg-light">
|
||
<div class="container">
|
||
<div class="row">
|
||
<div class="col-lg-8 mx-auto">
|
||
<div class="card">
|
||
<div class="card-header text-center">
|
||
<h5 class="mb-0"><i class="fas fa-calendar-alt me-2"></i>Календарь доступности гидов</h5>
|
||
</div>
|
||
<div class="card-body">
|
||
<div id="guides-calendar-container"></div>
|
||
</div>
|
||
</div>
|
||
</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 fa-3x text-muted mb-3"></i>
|
||
<h3>Гиды не найдены</h3>
|
||
<p class="text-muted">В данный момент нет доступных гидов.</p>
|
||
</div>
|
||
<% } else { %>
|
||
<div class="row">
|
||
<% guides.forEach(function(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/placeholder.jpg" 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">
|
||
<%
|
||
let percentage = parseFloat(guide.rating_percentage || 0);
|
||
%>
|
||
<%= Math.round(percentage) %>%
|
||
</span>
|
||
<small class="text-muted ms-1">
|
||
<%
|
||
let likes = parseInt(guide.likes || 0);
|
||
let dislikes = parseInt(guide.dislikes || 0);
|
||
let total = likes + dislikes;
|
||
%>
|
||
(<%= likes %>/<%= total %>)
|
||
</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>
|
||
</button>
|
||
<button class="btn btn-sm btn-outline-danger dislike-btn" data-rating="-1">
|
||
<i class="fas fa-thumbs-down"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mb-2">
|
||
<% if (guide.specialization === 'city') { %>
|
||
<span class="badge bg-primary">
|
||
<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-info">
|
||
<i class="fas fa-fish me-1"></i>Морская рыбалка
|
||
</span>
|
||
<% } %>
|
||
</div>
|
||
|
||
<div class="mb-2">
|
||
<small class="text-muted">
|
||
<i class="fas fa-briefcase me-1"></i>
|
||
Опыт: <%= guide.experience || 0 %> лет
|
||
</small>
|
||
</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>
|
||
<% } %>
|
||
|
||
<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="text-end">
|
||
<strong class="text-success">
|
||
₩<%= parseInt(guide.hourly_rate || 0).toLocaleString('ko-KR') %>/час
|
||
</strong>
|
||
</div>
|
||
<% } %>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<% }); %>
|
||
</div>
|
||
<% } %>
|
||
</div>
|
||
</section>
|
||
|
||
<script>
|
||
// Guide card click handler and rating system
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
const guideCards = document.querySelectorAll('.guide-card');
|
||
|
||
guideCards.forEach(card => {
|
||
card.addEventListener('click', function(e) {
|
||
// Don't trigger card click if clicking on rating buttons
|
||
if (!e.target.closest('.rating-buttons')) {
|
||
const guideId = this.dataset.guideId;
|
||
// Redirect to guide details or show modal
|
||
window.location.href = `/guides/${guideId}`;
|
||
}
|
||
});
|
||
|
||
// Add hover effect
|
||
card.addEventListener('mouseenter', function() {
|
||
this.style.transform = 'translateY(-5px)';
|
||
});
|
||
|
||
card.addEventListener('mouseleave', function() {
|
||
this.style.transform = 'translateY(0)';
|
||
});
|
||
});
|
||
|
||
// Rating system
|
||
document.addEventListener('click', function(e) {
|
||
if (e.target.closest('.like-btn') || e.target.closest('.dislike-btn')) {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
|
||
const button = e.target.closest('.like-btn') || e.target.closest('.dislike-btn');
|
||
const ratingDisplay = button.closest('.rating-display');
|
||
const targetType = ratingDisplay.dataset.targetType;
|
||
const targetId = ratingDisplay.dataset.targetId;
|
||
const rating = parseInt(button.dataset.rating);
|
||
|
||
// Send rating to server
|
||
fetch('/api/rating/' + targetType + '/' + targetId, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({ rating: rating })
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
// Update rating display
|
||
const percentageSpan = ratingDisplay.querySelector('.rating-percentage');
|
||
const countSpan = ratingDisplay.querySelector('small');
|
||
|
||
const percentage = data.percentage || 0;
|
||
const likes = data.likes || 0;
|
||
const total = data.total || 0;
|
||
|
||
percentageSpan.textContent = Math.round(percentage) + '%';
|
||
countSpan.textContent = `(${likes}/${total})`;
|
||
|
||
// Update button states
|
||
ratingDisplay.querySelectorAll('.rating-buttons button').forEach(btn => {
|
||
btn.classList.remove('btn-success', 'btn-danger');
|
||
btn.classList.add(btn.classList.contains('like-btn') ? 'btn-outline-success' : 'btn-outline-danger');
|
||
});
|
||
|
||
// Highlight selected button
|
||
if (rating === 1) {
|
||
button.classList.remove('btn-outline-success');
|
||
button.classList.add('btn-success');
|
||
} else if (rating === -1) {
|
||
button.classList.remove('btn-outline-danger');
|
||
button.classList.add('btn-danger');
|
||
}
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Ошибка при отправке рейтинга:', error);
|
||
});
|
||
}
|
||
});
|
||
});
|
||
</script> |