Files
tourrism_site/public/js/admin-custom.js
Andrey K. Choi b4e513e996 🚀 Korea Tourism Agency - Complete implementation
 Features:
- Modern tourism website with responsive design
- AdminJS admin panel with image editor integration
- PostgreSQL database with comprehensive schema
- Docker containerization
- Image upload and gallery management

🛠 Tech Stack:
- Backend: Node.js + Express.js
- Database: PostgreSQL 13+
- Frontend: HTML/CSS/JS with responsive design
- Admin: AdminJS with custom components
- Deployment: Docker + Docker Compose
- Image Processing: Sharp with optimization

📱 Admin Features:
- Routes/Tours management (city, mountain, fishing)
- Guides profiles with specializations
- Articles and blog system
- Image editor with upload/gallery/URL options
- User management and authentication
- Responsive admin interface

🎨 Design:
- Korean tourism focused branding
- Mobile-first responsive design
- Custom CSS with modern aesthetics
- Image optimization and gallery
- SEO-friendly structure

🔒 Security:
- Helmet.js security headers
- bcrypt password hashing
- Input validation and sanitization
- CORS protection
- Environment variables
2025-11-30 00:53:15 +09:00

406 lines
13 KiB
JavaScript
Raw 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.

/* Korea Tourism Agency Admin Panel Custom Scripts */
// Функция для открытия редактора изображений
function openImageEditor(fieldName, currentValue) {
const editorUrl = `/image-editor.html?field=${fieldName}&current=${encodeURIComponent(currentValue || '')}`;
const editorWindow = window.open(editorUrl, 'imageEditor', 'width=1200,height=800,scrollbars=yes,resizable=yes');
// Слушаем сообщения от редактора
const messageHandler = (event) => {
if (event.origin !== window.location.origin) return;
if (event.data.type === 'imageSelected' && event.data.targetField === fieldName) {
const field = document.querySelector(`input[name="${fieldName}"], input[id="${fieldName}"]`);
if (field) {
field.value = event.data.path;
field.dispatchEvent(new Event('change', { bubbles: true }));
// Обновляем превью если есть
updateImagePreview(fieldName, event.data.path);
}
window.removeEventListener('message', messageHandler);
editorWindow.close();
}
};
window.addEventListener('message', messageHandler);
// Очистка обработчика при закрытии окна
const checkClosed = setInterval(() => {
if (editorWindow.closed) {
window.removeEventListener('message', messageHandler);
clearInterval(checkClosed);
}
}, 1000);
}
// Функция обновления превью изображения
function updateImagePreview(fieldName, imagePath) {
const previewId = `${fieldName}_preview`;
let preview = document.getElementById(previewId);
if (!preview) {
// Создаем превью если его нет
const field = document.querySelector(`input[name="${fieldName}"], input[id="${fieldName}"]`);
if (field) {
preview = document.createElement('img');
preview.id = previewId;
preview.className = 'img-thumbnail mt-2';
preview.style.maxWidth = '200px';
preview.style.maxHeight = '200px';
field.parentNode.appendChild(preview);
}
}
if (preview) {
preview.src = imagePath || '/images/placeholders/no-image.png';
preview.alt = 'Preview';
}
}
// Функция добавления кнопки редактора к полю
function addImageEditorButton(field) {
const fieldName = field.name || field.id;
if (!fieldName) return;
// Проверяем, не добавлена ли уже кнопка
if (field.parentNode.querySelector('.image-editor-btn')) return;
const wrapper = document.createElement('div');
wrapper.className = 'input-group';
const button = document.createElement('button');
button.type = 'button';
button.className = 'btn btn-outline-secondary image-editor-btn';
button.innerHTML = '<i class="fas fa-images"></i> Выбрать';
button.onclick = () => openImageEditor(fieldName, field.value);
const buttonWrapper = document.createElement('div');
buttonWrapper.className = 'input-group-append';
buttonWrapper.appendChild(button);
// Перестраиваем структуру
field.parentNode.insertBefore(wrapper, field);
wrapper.appendChild(field);
wrapper.appendChild(buttonWrapper);
// Добавляем превью если есть значение
if (field.value) {
updateImagePreview(fieldName, field.value);
}
}
$(document).ready(function() {
// Initialize tooltips
$('[data-toggle="tooltip"]').tooltip();
// Initialize popovers
$('[data-toggle="popover"]').popover();
// Auto-hide alerts after 5 seconds
setTimeout(function() {
$('.alert').fadeOut('slow');
}, 5000);
// Добавляем кнопки редактора к полям изображений
$('input[type="text"], input[type="url"]').each(function() {
const field = this;
const fieldName = field.name || field.id || '';
// Проверяем, относится ли поле к изображениям
if (fieldName.includes('image') || fieldName.includes('photo') || fieldName.includes('avatar') ||
fieldName.includes('picture') || fieldName.includes('thumbnail') || fieldName.includes('banner') ||
$(field).closest('label').text().toLowerCase().includes('изображение') ||
$(field).closest('label').text().toLowerCase().includes('картинка') ||
$(field).closest('label').text().toLowerCase().includes('фото')) {
addImageEditorButton(field);
}
});
// Обработчик для динамически добавляемых полей
$(document).on('focus', 'input[type="text"], input[type="url"]', function() {
const field = this;
const fieldName = field.name || field.id || '';
if ((fieldName.includes('image') || fieldName.includes('photo') || fieldName.includes('avatar') ||
fieldName.includes('picture') || fieldName.includes('thumbnail') || fieldName.includes('banner')) &&
!field.parentNode.querySelector('.image-editor-btn')) {
addImageEditorButton(field);
}
});
// Confirm delete actions
$('.btn-delete').on('click', function(e) {
e.preventDefault();
const item = $(this).data('item') || 'item';
const url = $(this).attr('href') || $(this).data('url');
if (confirm(`Are you sure you want to delete this ${item}?`)) {
if ($(this).data('method') === 'DELETE') {
// AJAX delete
$.ajax({
url: url,
method: 'DELETE',
success: function(response) {
if (response.success) {
location.reload();
} else {
alert('Error: ' + response.message);
}
},
error: function() {
alert('An error occurred while deleting.');
}
});
} else {
// Regular form submission or redirect
window.location.href = url;
}
}
});
// Form validation enhancement
$('form').on('submit', function() {
const submitBtn = $(this).find('button[type="submit"]');
submitBtn.prop('disabled', true);
submitBtn.html('<i class="fas fa-spinner fa-spin"></i> Processing...');
});
// Image preview functionality
function readURL(input, target) {
if (input.files && input.files[0]) {
const reader = new FileReader();
reader.onload = function(e) {
$(target).attr('src', e.target.result).show();
};
reader.readAsDataURL(input.files[0]);
}
}
$('input[type="file"]').on('change', function() {
const targetImg = $(this).closest('.form-group').find('.img-preview');
if (targetImg.length) {
readURL(this, targetImg);
}
});
// Auto-save draft functionality for forms
let autoSaveTimer;
$('textarea, input[type="text"]').on('input', function() {
clearTimeout(autoSaveTimer);
autoSaveTimer = setTimeout(function() {
// Auto-save logic here
console.log('Auto-saving draft...');
}, 2000);
});
// Enhanced DataTables configuration
if (typeof $.fn.dataTable !== 'undefined') {
$('.data-table').each(function() {
$(this).DataTable({
responsive: true,
lengthChange: false,
autoWidth: false,
pageLength: 25,
language: {
search: "Search:",
lengthMenu: "Show _MENU_ entries",
info: "Showing _START_ to _END_ of _TOTAL_ entries",
paginate: {
first: "First",
last: "Last",
next: "Next",
previous: "Previous"
}
},
dom: '<"row"<"col-sm-6"l><"col-sm-6"f>>' +
'<"row"<"col-sm-12"tr>>' +
'<"row"<"col-sm-5"i><"col-sm-7"p>>',
});
});
}
// Status toggle functionality
$('.status-toggle').on('change', function() {
const checkbox = $(this);
const id = checkbox.data('id');
const type = checkbox.data('type');
const field = checkbox.data('field');
const isChecked = checkbox.is(':checked');
$.ajax({
url: `/admin/${type}/${id}/toggle`,
method: 'POST',
data: {
field: field,
value: isChecked
},
success: function(response) {
if (response.success) {
showNotification('Status updated successfully!', 'success');
} else {
checkbox.prop('checked', !isChecked);
showNotification('Error updating status: ' + response.message, 'error');
}
},
error: function() {
checkbox.prop('checked', !isChecked);
showNotification('Error updating status', 'error');
}
});
});
// Notification system
function showNotification(message, type = 'info') {
const alertClass = type === 'success' ? 'alert-success' :
type === 'error' ? 'alert-danger' :
type === 'warning' ? 'alert-warning' : 'alert-info';
const notification = `
<div class="alert ${alertClass} alert-dismissible fade show" role="alert" style="position: fixed; top: 20px; right: 20px; z-index: 1050; min-width: 300px;">
${message}
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
`;
$('body').append(notification);
// Auto-hide after 3 seconds
setTimeout(function() {
$('.alert').last().fadeOut('slow', function() {
$(this).remove();
});
}, 3000);
}
// Make notification function globally available
window.showNotification = showNotification;
// Quick search functionality
$('#quick-search').on('input', function() {
const searchTerm = $(this).val().toLowerCase();
const searchableElements = $('.searchable');
if (searchTerm === '') {
searchableElements.show();
} else {
searchableElements.each(function() {
const text = $(this).text().toLowerCase();
if (text.includes(searchTerm)) {
$(this).show();
} else {
$(this).hide();
}
});
}
});
// Bulk actions functionality
$('#select-all').on('change', function() {
$('.item-checkbox').prop('checked', $(this).is(':checked'));
updateBulkActionButtons();
});
$('.item-checkbox').on('change', function() {
updateBulkActionButtons();
// Update select-all checkbox state
const totalCheckboxes = $('.item-checkbox').length;
const checkedCheckboxes = $('.item-checkbox:checked').length;
if (checkedCheckboxes === 0) {
$('#select-all').prop('indeterminate', false).prop('checked', false);
} else if (checkedCheckboxes === totalCheckboxes) {
$('#select-all').prop('indeterminate', false).prop('checked', true);
} else {
$('#select-all').prop('indeterminate', true);
}
});
function updateBulkActionButtons() {
const checkedItems = $('.item-checkbox:checked').length;
if (checkedItems > 0) {
$('.bulk-actions').show();
$('.bulk-count').text(checkedItems);
} else {
$('.bulk-actions').hide();
}
}
// Image upload preview
$('.image-upload').on('change', function() {
const file = this.files[0];
const preview = $(this).siblings('.image-preview');
if (file) {
const reader = new FileReader();
reader.onload = function(e) {
preview.html(`<img src="${e.target.result}" class="img-thumbnail" style="max-width: 200px; max-height: 200px;">`);
};
reader.readAsDataURL(file);
}
});
// Chart initialization (if Chart.js is available)
if (typeof Chart !== 'undefined' && $('#dashboard-chart').length) {
const ctx = document.getElementById('dashboard-chart').getContext('2d');
new Chart(ctx, {
type: 'line',
data: {
labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'],
datasets: [{
label: 'Bookings',
data: [12, 19, 3, 5, 2, 3],
borderColor: '#3b82f6',
backgroundColor: 'rgba(59, 130, 246, 0.1)',
tension: 0.4
}]
},
options: {
responsive: true,
plugins: {
legend: {
position: 'top',
}
},
scales: {
y: {
beginAtZero: true
}
}
}
});
}
});
// Global utility functions
window.AdminUtils = {
// Format currency
formatCurrency: function(amount) {
return '₩' + new Intl.NumberFormat('ko-KR').format(amount);
},
// Format date
formatDate: function(date) {
return new Date(date).toLocaleDateString('ko-KR');
},
// Validate email
isValidEmail: function(email) {
const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return re.test(email);
},
// Show loading spinner
showLoading: function(element) {
$(element).addClass('loading').append('<div class="spinner"></div>');
},
// Hide loading spinner
hideLoading: function(element) {
$(element).removeClass('loading').find('.spinner').remove();
}
};