Initial commit: Korea Tourism Agency website with AdminJS
- Full-stack Node.js/Express application with PostgreSQL - Modern ES modules architecture - AdminJS admin panel with Sequelize ORM - Tourism routes, guides, articles, bookings management - Responsive Bootstrap 5 frontend - Docker containerization with docker-compose - Complete database schema with migrations - Authentication system for admin panel - Dynamic placeholder images for tour categories
This commit is contained in:
108
views/guides/index.ejs
Normal file
108
views/guides/index.ejs
Normal file
@@ -0,0 +1,108 @@
|
||||
<!-- Hero Section -->
|
||||
<section class="hero-section 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">
|
||||
<% 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">
|
||||
<h5 class="card-title"><%= guide.name %></h5>
|
||||
|
||||
<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>
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
<% if (guide.bio) { %>
|
||||
<p class="card-text text-muted flex-grow-1"><%= truncateText(guide.bio, 120) %></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">
|
||||
<%- generateStars(guide.avg_rating) %>
|
||||
<small class="text-muted ms-2">(<%= parseFloat(guide.avg_rating).toFixed(1) %>)</small>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<div>
|
||||
<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>
|
||||
260
views/guides/profile.ejs
Normal file
260
views/guides/profile.ejs
Normal file
@@ -0,0 +1,260 @@
|
||||
<!-- Hero Section -->
|
||||
<section class="hero-section bg-primary text-white py-5">
|
||||
<div class="container">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-lg-4 text-center text-lg-start">
|
||||
<% if (guide.image_url && guide.image_url.trim()) { %>
|
||||
<img src="<%= guide.image_url %>" alt="<%= guide.name %>"
|
||||
class="img-fluid rounded-circle shadow-lg mb-4 mb-lg-0"
|
||||
style="width: 200px; height: 200px; object-fit: cover;">
|
||||
<% } else { %>
|
||||
<img src="/images/placeholder.jpg" alt="<%= guide.name %>"
|
||||
class="img-fluid rounded-circle shadow-lg mb-4 mb-lg-0"
|
||||
style="width: 200px; height: 200px; object-fit: cover;">
|
||||
<% } %>
|
||||
</div>
|
||||
<div class="col-lg-8">
|
||||
<h1 class="display-4 fw-bold mb-3"><%= guide.name %></h1>
|
||||
|
||||
<div class="mb-4">
|
||||
<% if (guide.specialization === 'city') { %>
|
||||
<span class="badge bg-info fs-6 me-2">
|
||||
<i class="fas fa-city me-1"></i>Городские туры
|
||||
</span>
|
||||
<% } else if (guide.specialization === 'mountain') { %>
|
||||
<span class="badge bg-success fs-6 me-2">
|
||||
<i class="fas fa-mountain me-1"></i>Горные походы
|
||||
</span>
|
||||
<% } else if (guide.specialization === 'fishing') { %>
|
||||
<span class="badge bg-info fs-6 me-2">
|
||||
<i class="fas fa-fish me-1"></i>Рыбалка
|
||||
</span>
|
||||
<% } %>
|
||||
|
||||
<span class="badge bg-warning text-dark fs-6">
|
||||
<i class="fas fa-star me-1"></i>
|
||||
<%= guide.experience %> лет опыта
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<% if (guide.languages) { %>
|
||||
<p class="mb-3">
|
||||
<strong>Языки:</strong> <%= guide.languages %>
|
||||
</p>
|
||||
<% } %>
|
||||
|
||||
<% if (guide.avg_rating) { %>
|
||||
<div class="mb-3">
|
||||
<span class="me-2">Рейтинг:</span>
|
||||
<%- generateStars(guide.avg_rating) %>
|
||||
<span class="ms-2">(<%= parseFloat(guide.avg_rating).toFixed(1) %>/5)</span>
|
||||
<% if (guide.review_count) { %>
|
||||
<small class="text-light-50 ms-2"><%= guide.review_count %> отзывов</small>
|
||||
<% } %>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<p class="mb-1">
|
||||
<i class="fas fa-route me-2"></i>
|
||||
<strong><%= guide.route_count || 0 %></strong> туров
|
||||
</p>
|
||||
</div>
|
||||
<% if (guide.phone) { %>
|
||||
<div class="col-md-6">
|
||||
<p class="mb-1">
|
||||
<i class="fas fa-phone me-2"></i>
|
||||
<a href="tel:<%= guide.phone %>" class="text-light"><%= guide.phone %></a>
|
||||
</p>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Guide Details -->
|
||||
<section class="py-5">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
<!-- Bio -->
|
||||
<% if (guide.bio) { %>
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h4 class="card-title mb-0">
|
||||
<i class="fas fa-user me-2"></i>О гиде
|
||||
</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="mb-0"><%= guide.bio %></p>
|
||||
</div>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<!-- Tours -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h4 class="card-title mb-0">
|
||||
<i class="fas fa-route me-2"></i>Туры с этим гидом
|
||||
</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<% if (routes && routes.length > 0) { %>
|
||||
<div class="row">
|
||||
<% routes.forEach(route => { %>
|
||||
<div class="col-md-6 mb-3">
|
||||
<div class="card h-100">
|
||||
<%
|
||||
let placeholderImage = '/images/placeholder.jpg';
|
||||
if (route.type === 'city') {
|
||||
placeholderImage = '/images/city-tour-placeholder.webp';
|
||||
} else if (route.type === 'mountain') {
|
||||
placeholderImage = '/images/mountain-placeholder.jpg';
|
||||
} else if (route.type === 'fishing') {
|
||||
placeholderImage = '/images/fish-placeholder.jpg';
|
||||
}
|
||||
%>
|
||||
<% if (route.image_url && route.image_url.trim()) { %>
|
||||
<img src="<%= route.image_url %>" class="card-img-top"
|
||||
alt="<%= route.title %>" style="height: 150px; object-fit: cover;">
|
||||
<% } else { %>
|
||||
<img src="<%= placeholderImage %>" class="card-img-top"
|
||||
alt="<%= route.title %>" style="height: 150px; object-fit: cover;">
|
||||
<% } %>
|
||||
<div class="card-body">
|
||||
<h6 class="card-title"><%= route.title %></h6>
|
||||
<p class="card-text text-muted small"><%= truncateText(route.description, 80) %></p>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<strong class="text-primary">₩<%= formatCurrency(route.price) %></strong>
|
||||
<small class="text-muted"><%= route.duration %> дн.</small>
|
||||
</div>
|
||||
<a href="/routes/<%= route.id %>" class="btn btn-outline-primary btn-sm mt-2 w-100">
|
||||
Подробнее
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% }); %>
|
||||
</div>
|
||||
<% } else { %>
|
||||
<p class="text-muted mb-0">В данный момент у этого гида нет активных туров.</p>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Reviews -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h4 class="card-title mb-0">
|
||||
<i class="fas fa-star me-2"></i>Отзывы
|
||||
</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<% if (reviews && reviews.length > 0) { %>
|
||||
<% reviews.forEach(review => { %>
|
||||
<div class="border-bottom pb-3 mb-3">
|
||||
<div class="d-flex justify-content-between align-items-start mb-2">
|
||||
<div>
|
||||
<h6 class="mb-1"><%= review.author_name %></h6>
|
||||
<% if (review.route_title) { %>
|
||||
<small class="text-muted">Тур: <%= review.route_title %></small>
|
||||
<% } %>
|
||||
</div>
|
||||
<div class="text-end">
|
||||
<% if (review.rating) { %>
|
||||
<div class="mb-1">
|
||||
<%- generateStars(review.rating) %>
|
||||
</div>
|
||||
<% } %>
|
||||
<small class="text-muted"><%= formatDateShort(review.created_at) %></small>
|
||||
</div>
|
||||
</div>
|
||||
<p class="mb-0"><%= review.comment %></p>
|
||||
</div>
|
||||
<% }); %>
|
||||
<% } else { %>
|
||||
<p class="text-muted mb-0">Пока нет отзывов об этом гиде.</p>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<div class="col-lg-4">
|
||||
<!-- Contact Card -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="fas fa-address-card me-2"></i>Контактная информация
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<% if (guide.phone) { %>
|
||||
<p class="mb-2">
|
||||
<i class="fas fa-phone me-2"></i>
|
||||
<a href="tel:<%= guide.phone %>"><%= guide.phone %></a>
|
||||
</p>
|
||||
<% } %>
|
||||
|
||||
<% if (guide.email) { %>
|
||||
<p class="mb-2">
|
||||
<i class="fas fa-envelope me-2"></i>
|
||||
<a href="mailto:<%= guide.email %>"><%= guide.email %></a>
|
||||
</p>
|
||||
<% } %>
|
||||
|
||||
<% if (guide.languages) { %>
|
||||
<p class="mb-2">
|
||||
<i class="fas fa-language me-2"></i>
|
||||
<%= guide.languages %>
|
||||
</p>
|
||||
<% } %>
|
||||
|
||||
<a href="/contact" class="btn btn-primary w-100 mt-3">
|
||||
<i class="fas fa-envelope me-1"></i>Связаться с гидом
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stats Card -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="fas fa-chart-bar me-2"></i>Статистика
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row text-center">
|
||||
<div class="col-6 mb-3">
|
||||
<div class="border-end">
|
||||
<h4 class="text-primary mb-1"><%= guide.route_count || 0 %></h4>
|
||||
<small class="text-muted">Туров</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 mb-3">
|
||||
<h4 class="text-success mb-1"><%= guide.experience %></h4>
|
||||
<small class="text-muted">Лет опыта</small>
|
||||
</div>
|
||||
</div>
|
||||
<% if (guide.avg_rating) { %>
|
||||
<div class="text-center">
|
||||
<h4 class="text-warning mb-1"><%= parseFloat(guide.avg_rating).toFixed(1) %></h4>
|
||||
<small class="text-muted">Средний рейтинг</small>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Back Button -->
|
||||
<div class="mt-4">
|
||||
<a href="/guides" class="btn btn-outline-primary w-100">
|
||||
<i class="fas fa-arrow-left me-1"></i>Все гиды
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
Reference in New Issue
Block a user