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:
289
views/routes/detail.ejs
Normal file
289
views/routes/detail.ejs
Normal file
@@ -0,0 +1,289 @@
|
||||
<!-- Hero Section -->
|
||||
<section class="hero-section bg-primary text-white">
|
||||
<div class="container py-5">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-lg-8">
|
||||
<h1 class="display-4 fw-bold mb-3"><%= route.title %></h1>
|
||||
<p class="lead mb-4"><%= route.description %></p>
|
||||
|
||||
<div class="d-flex flex-wrap gap-2 mb-4">
|
||||
<% if (route.type === 'city') { %>
|
||||
<span class="badge bg-info fs-6">
|
||||
<i class="fas fa-city me-1"></i>Городской тур
|
||||
</span>
|
||||
<% } else if (route.type === 'mountain') { %>
|
||||
<span class="badge bg-success fs-6">
|
||||
<i class="fas fa-mountain me-1"></i>Горный поход
|
||||
</span>
|
||||
<% } else if (route.type === 'fishing') { %>
|
||||
<span class="badge bg-info fs-6">
|
||||
<i class="fas fa-fish me-1"></i>Рыбалка
|
||||
</span>
|
||||
<% } %>
|
||||
|
||||
<span class="badge <%= route.difficulty_level === 'easy' ? 'bg-success' : route.difficulty_level === 'moderate' ? 'bg-warning' : 'bg-danger' %> fs-6">
|
||||
<%= route.difficulty_level === 'easy' ? 'Легко' : route.difficulty_level === 'moderate' ? 'Средне' : 'Сложно' %>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<div class="bg-white text-dark p-4 rounded shadow">
|
||||
<h4 class="text-primary mb-3">Детали тура</h4>
|
||||
<div class="mb-3">
|
||||
<strong class="d-block">Цена</strong>
|
||||
<span class="h4 text-primary">₩<%= formatCurrency(route.price) %></span>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<strong class="d-block">Продолжительность</strong>
|
||||
<span><%= route.duration %> дней</span>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<strong class="d-block">Максимум участников</strong>
|
||||
<span><%= route.max_group_size %> человек</span>
|
||||
</div>
|
||||
<a href="/contact" class="btn btn-primary w-100">
|
||||
<i class="fas fa-calendar-plus me-1"></i>Забронировать
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="container py-5">
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
<!-- Route Image -->
|
||||
<%
|
||||
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="img-fluid rounded mb-4" alt="<%= route.title %>">
|
||||
<% } else { %>
|
||||
<img src="<%= placeholderImage %>" class="img-fluid rounded mb-4" alt="<%= route.title %>">
|
||||
<% } %>
|
||||
|
||||
<!-- Route Content -->
|
||||
<div class="mb-5">
|
||||
<h2 class="mb-3">Описание маршрута</h2>
|
||||
<div class="content">
|
||||
<%= route.content || 'Подробное описание маршрута будет доступно в ближайшее время.' %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Included Services -->
|
||||
<% if (route.included_services && route.included_services.length > 0) { %>
|
||||
<div class="mb-5">
|
||||
<h3 class="mb-3">Что включено</h3>
|
||||
<div class="row">
|
||||
<% route.included_services.forEach(service => { %>
|
||||
<div class="col-md-6 mb-2">
|
||||
<i class="fas fa-check text-success me-2"></i><%= service %>
|
||||
</div>
|
||||
<% }); %>
|
||||
</div>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<!-- Reviews Section -->
|
||||
<div class="mb-5">
|
||||
<h3 class="mb-4">Отзывы (<%= reviews.length %>)</h3>
|
||||
|
||||
<!-- Review Form -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Оставить отзыв</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form id="reviewForm">
|
||||
<input type="hidden" name="route_id" value="<%= route.id %>">
|
||||
<% if (route.guide_id) { %>
|
||||
<input type="hidden" name="guide_id" value="<%= route.guide_id %>">
|
||||
<% } %>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="customer_name" class="form-label">Ваше имя *</label>
|
||||
<input type="text" class="form-control" id="customer_name" name="customer_name" required>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="customer_email" class="form-label">Email</label>
|
||||
<input type="email" class="form-control" id="customer_email" name="customer_email">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="rating" class="form-label">Оценка *</label>
|
||||
<div class="star-rating">
|
||||
<input type="radio" id="star5" name="rating" value="5">
|
||||
<label for="star5" title="Отлично"><i class="fas fa-star"></i></label>
|
||||
<input type="radio" id="star4" name="rating" value="4">
|
||||
<label for="star4" title="Хорошо"><i class="fas fa-star"></i></label>
|
||||
<input type="radio" id="star3" name="rating" value="3">
|
||||
<label for="star3" title="Нормально"><i class="fas fa-star"></i></label>
|
||||
<input type="radio" id="star2" name="rating" value="2">
|
||||
<label for="star2" title="Плохо"><i class="fas fa-star"></i></label>
|
||||
<input type="radio" id="star1" name="rating" value="1">
|
||||
<label for="star1" title="Очень плохо"><i class="fas fa-star"></i></label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="comment" class="form-label">Комментарий *</label>
|
||||
<textarea class="form-control" id="comment" name="comment" rows="4"
|
||||
placeholder="Поделитесь впечатлениями о туре..." required></textarea>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-paper-plane me-1"></i>Отправить отзыв
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Reviews List -->
|
||||
<% if (reviews.length === 0) { %>
|
||||
<p class="text-muted">Пока нет отзывов об этом туре. Будьте первым!</p>
|
||||
<% } else { %>
|
||||
<% reviews.forEach(review => { %>
|
||||
<div class="card mb-3">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-start mb-2">
|
||||
<h6 class="mb-0"><%= review.customer_name %></h6>
|
||||
<div>
|
||||
<%- generateStars(review.rating) %>
|
||||
</div>
|
||||
</div>
|
||||
<p class="card-text"><%= review.comment %></p>
|
||||
<small class="text-muted"><%= formatDate(review.created_at) %></small>
|
||||
</div>
|
||||
</div>
|
||||
<% }); %>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<div class="col-lg-4">
|
||||
<!-- Guide Info -->
|
||||
<% if (route.guide_name) { %>
|
||||
<div class="card mb-4">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Ваш гид</h5>
|
||||
<% if (route.guide_image) { %>
|
||||
<img src="<%= route.guide_image %>" class="rounded-circle mb-3" width="80" height="80" style="object-fit: cover;" alt="<%= route.guide_name %>">
|
||||
<% } %>
|
||||
<h6><%= route.guide_name %></h6>
|
||||
<% if (route.guide_bio) { %>
|
||||
<p class="text-muted small"><%= route.guide_bio %></p>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<!-- Contact Card -->
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Нужна помощь?</h5>
|
||||
<p class="card-text">Свяжитесь с нами для получения дополнительной информации о туре.</p>
|
||||
<div class="mb-3">
|
||||
<strong class="d-block">Телефон</strong>
|
||||
<span>+7 (495) 123-45-67</span>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<strong class="d-block">Email</strong>
|
||||
<span>info@koreatour.ru</span>
|
||||
</div>
|
||||
<a href="/contact" class="btn btn-outline-primary w-100">
|
||||
<i class="fas fa-envelope me-1"></i>Написать нам
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Similar Tours -->
|
||||
<section class="bg-light py-5">
|
||||
<div class="container">
|
||||
<h2 class="text-center mb-5">Похожие туры</h2>
|
||||
<div class="text-center">
|
||||
<a href="/routes?type=<%= route.type %>" class="btn btn-primary">
|
||||
Посмотреть все туры категории
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.star-rating {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.star-rating input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.star-rating label {
|
||||
color: #ddd;
|
||||
font-size: 1.5rem;
|
||||
margin: 0 2px;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.star-rating input:checked ~ label,
|
||||
.star-rating label:hover,
|
||||
.star-rating label:hover ~ label {
|
||||
color: #ffc107;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
// Review form submission
|
||||
document.getElementById('reviewForm').addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData(this);
|
||||
const data = Object.fromEntries(formData);
|
||||
|
||||
// Validate rating
|
||||
if (!data.rating) {
|
||||
alert('Пожалуйста, выберите оценку');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/reviews', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
alert(result.message);
|
||||
this.reset();
|
||||
// Optionally reload the page to show new review
|
||||
// location.reload();
|
||||
} else {
|
||||
alert(result.message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error submitting review:', error);
|
||||
alert('Произошла ошибка при отправке отзыва');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
131
views/routes/index.ejs
Normal file
131
views/routes/index.ejs
Normal file
@@ -0,0 +1,131 @@
|
||||
<!-- Hero Section -->
|
||||
<section class="hero-section bg-primary text-white text-center py-5">
|
||||
<div class="container">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-lg-8 mx-auto">
|
||||
<h1 class="display-4 fw-bold mb-4">Туры по Корее</h1>
|
||||
<p class="lead mb-4">Откройте для себя удивительную Корею с нашими профессиональными гидами</p>
|
||||
|
||||
<!-- Filter Buttons -->
|
||||
<div class="btn-group mb-4" role="group">
|
||||
<a href="/routes" class="btn <%= currentType === 'all' ? 'btn-light' : 'btn-outline-light' %>">
|
||||
<i class="fas fa-list me-1"></i>Все туры
|
||||
</a>
|
||||
<a href="/routes?type=city" class="btn <%= currentType === 'city' ? 'btn-light' : 'btn-outline-light' %>">
|
||||
<i class="fas fa-city me-1"></i>Городские туры
|
||||
</a>
|
||||
<a href="/routes?type=mountain" class="btn <%= currentType === 'mountain' ? 'btn-light' : 'btn-outline-light' %>">
|
||||
<i class="fas fa-mountain me-1"></i>Горные походы
|
||||
</a>
|
||||
<a href="/routes?type=fishing" class="btn <%= currentType === 'fishing' ? 'btn-light' : 'btn-outline-light' %>">
|
||||
<i class="fas fa-fish me-1"></i>Морская рыбалка
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Routes Grid -->
|
||||
<section class="py-5">
|
||||
<div class="container">
|
||||
<% if (routes.length === 0) { %>
|
||||
<div class="text-center py-5">
|
||||
<i class="fas fa-map text-muted" style="font-size: 4rem;"></i>
|
||||
<h3 class="mt-3 text-muted">Туры не найдены</h3>
|
||||
<p class="text-muted">В данной категории пока нет доступных туров.</p>
|
||||
<a href="/routes" class="btn btn-primary">Посмотреть все туры</a>
|
||||
</div>
|
||||
<% } else { %>
|
||||
<div class="row">
|
||||
<% routes.forEach(route => { %>
|
||||
<div class="col-lg-4 col-md-6 mb-4">
|
||||
<div class="card h-100 shadow-sm">
|
||||
<%
|
||||
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: 250px; object-fit: cover;">
|
||||
<% } else { %>
|
||||
<img src="<%= placeholderImage %>" class="card-img-top" alt="<%= route.title %>" style="height: 250px; object-fit: cover;">
|
||||
<% } %>
|
||||
|
||||
<!-- Featured Badge -->
|
||||
<% if (route.is_featured) { %>
|
||||
<span class="badge bg-warning text-dark position-absolute top-0 start-0 m-2">
|
||||
<i class="fas fa-star me-1"></i>Рекомендуем
|
||||
</span>
|
||||
<% } %>
|
||||
|
||||
<div class="card-body d-flex flex-column">
|
||||
<div class="mb-2">
|
||||
<% if (route.type === 'city') { %>
|
||||
<span class="badge bg-info text-dark">
|
||||
<i class="fas fa-city me-1"></i>Городской тур
|
||||
</span>
|
||||
<% } else if (route.type === 'mountain') { %>
|
||||
<span class="badge bg-success">
|
||||
<i class="fas fa-mountain me-1"></i>Горный поход
|
||||
</span>
|
||||
<% } else if (route.type === 'fishing') { %>
|
||||
<span class="badge bg-primary">
|
||||
<i class="fas fa-fish me-1"></i>Рыбалка
|
||||
</span>
|
||||
<% } %>
|
||||
|
||||
<% if (route.difficulty_level) { %>
|
||||
<span class="badge <%= route.difficulty_level === 'easy' ? 'bg-success' : route.difficulty_level === 'moderate' ? 'bg-warning' : 'bg-danger' %>">
|
||||
<%= route.difficulty_level === 'easy' ? 'Легко' : route.difficulty_level === 'moderate' ? 'Средне' : 'Сложно' %>
|
||||
</span>
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
<h5 class="card-title"><%= route.title %></h5>
|
||||
<p class="card-text text-muted flex-grow-1"><%= route.description %></p>
|
||||
|
||||
<div class="mt-auto">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<div>
|
||||
<strong class="text-primary h5">₩<%= formatCurrency(route.price) %></strong>
|
||||
</div>
|
||||
<small class="text-muted">
|
||||
<i class="fas fa-clock me-1"></i><%= route.duration %> дн.
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<% if (route.guide_name) { %>
|
||||
<p class="text-muted mb-2">
|
||||
<i class="fas fa-user-tie me-1"></i>Гид: <%= route.guide_name %>
|
||||
</p>
|
||||
<% } %>
|
||||
|
||||
<a href="/routes/<%= route.id %>" class="btn btn-primary w-100">
|
||||
<i class="fas fa-info-circle 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>
|
||||
Reference in New Issue
Block a user