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
218
public/css/admin-custom.css
Normal file
@@ -0,0 +1,218 @@
|
||||
/* Korea Tourism Agency Admin Panel Custom Styles */
|
||||
|
||||
/* Brand Customization */
|
||||
.brand-link {
|
||||
background-color: #1f2937 !important;
|
||||
}
|
||||
|
||||
.brand-text {
|
||||
color: #f8f9fa !important;
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
|
||||
/* Sidebar Customization */
|
||||
.main-sidebar {
|
||||
background: linear-gradient(180deg, #1f2937 0%, #111827 100%) !important;
|
||||
}
|
||||
|
||||
.nav-sidebar .nav-item > .nav-link {
|
||||
color: #d1d5db !important;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.nav-sidebar .nav-item > .nav-link:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1) !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.nav-sidebar .nav-item > .nav-link.active {
|
||||
background-color: #3b82f6 !important;
|
||||
color: #ffffff !important;
|
||||
border-radius: 0.375rem;
|
||||
margin: 0.125rem;
|
||||
}
|
||||
|
||||
/* Cards Enhancement */
|
||||
.card {
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
border: none;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border-radius: 0.5rem 0.5rem 0 0 !important;
|
||||
}
|
||||
|
||||
.card-header .card-title {
|
||||
color: white !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Small Boxes Enhancement */
|
||||
.small-box {
|
||||
border-radius: 0.75rem;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.small-box:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.small-box .icon {
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
}
|
||||
|
||||
/* Buttons Enhancement */
|
||||
.btn {
|
||||
border-radius: 0.375rem;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Table Enhancement */
|
||||
.table {
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.table thead th {
|
||||
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
||||
border: none;
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.table tbody tr:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
/* Form Enhancement */
|
||||
.form-control {
|
||||
border-radius: 0.375rem;
|
||||
border: 1px solid #d1d5db;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
/* Custom File Input */
|
||||
.custom-file-label {
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
.custom-file-input:focus ~ .custom-file-label {
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
/* Alert Enhancement */
|
||||
.alert {
|
||||
border-radius: 0.5rem;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
background: linear-gradient(135deg, #d4edda 0%, #c3e6cb 100%);
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.alert-danger {
|
||||
background: linear-gradient(135deg, #f8d7da 0%, #f1b0b7 100%);
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.alert-warning {
|
||||
background: linear-gradient(135deg, #fff3cd 0%, #ffeaa7 100%);
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
/* Badge Enhancement */
|
||||
.badge {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
padding: 0.375rem 0.75rem;
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
/* Loading Spinner */
|
||||
.loading {
|
||||
opacity: 0.7;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
border: 3px solid #f3f3f3;
|
||||
border-top: 3px solid #3b82f6;
|
||||
border-radius: 50%;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 10px auto;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Mobile Responsiveness */
|
||||
@media (max-width: 768px) {
|
||||
.card-header .card-title {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.small-box .inner h3 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.btn-group .btn {
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Dark Mode Support */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.card {
|
||||
background-color: #374151;
|
||||
color: #f9fafb;
|
||||
}
|
||||
|
||||
.table {
|
||||
color: #f9fafb;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
background-color: #374151;
|
||||
color: #f9fafb;
|
||||
border-color: #4b5563;
|
||||
}
|
||||
}
|
||||
|
||||
/* Korean Typography */
|
||||
.korean-text {
|
||||
font-family: 'Noto Sans KR', 'Malgun Gothic', '맑은 고딕', sans-serif;
|
||||
}
|
||||
|
||||
/* Success Animation */
|
||||
.success-animation {
|
||||
animation: successPulse 0.6s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes successPulse {
|
||||
0% { transform: scale(1); }
|
||||
50% { transform: scale(1.05); }
|
||||
100% { transform: scale(1); }
|
||||
}
|
||||
497
public/css/main.css
Normal file
@@ -0,0 +1,497 @@
|
||||
/* Korea Tourism Agency - Main Styles */
|
||||
|
||||
/* CSS Variables */
|
||||
:root {
|
||||
--primary-color: #2563eb;
|
||||
--primary-light: #3b82f6;
|
||||
--primary-dark: #1d4ed8;
|
||||
--secondary-color: #dc2626;
|
||||
--success-color: #059669;
|
||||
--warning-color: #d97706;
|
||||
--info-color: #0891b2;
|
||||
--light-color: #f8fafc;
|
||||
--dark-color: #0f172a;
|
||||
--gray-100: #f1f5f9;
|
||||
--gray-200: #e2e8f0;
|
||||
--gray-300: #cbd5e1;
|
||||
--gray-400: #94a3b8;
|
||||
--gray-500: #64748b;
|
||||
--gray-600: #475569;
|
||||
--gray-700: #334155;
|
||||
--gray-800: #1e293b;
|
||||
--gray-900: #0f172a;
|
||||
--korean-red: #c41e3a;
|
||||
--korean-blue: #003478;
|
||||
--font-korean: 'Noto Sans KR', 'Malgun Gothic', '맑은 고딕', sans-serif;
|
||||
--font-display: 'Playfair Display', serif;
|
||||
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
--shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
|
||||
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
/* Base Styles */
|
||||
body {
|
||||
font-family: var(--font-korean);
|
||||
line-height: 1.7;
|
||||
color: var(--gray-700);
|
||||
padding-top: 76px; /* Account for fixed navbar */
|
||||
}
|
||||
|
||||
.font-korean {
|
||||
font-family: var(--font-korean);
|
||||
}
|
||||
|
||||
.font-display {
|
||||
font-family: var(--font-display);
|
||||
}
|
||||
|
||||
/* Custom Bootstrap Overrides */
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, var(--primary-color) 0%, var(--primary-light) 100%);
|
||||
border: none;
|
||||
box-shadow: var(--shadow-md);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: linear-gradient(135deg, var(--primary-dark) 0%, var(--primary-color) 100%);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.text-gradient {
|
||||
background: linear-gradient(135deg, var(--korean-red) 0%, var(--korean-blue) 100%);
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
.bg-gradient-primary {
|
||||
background: linear-gradient(135deg, var(--primary-color) 0%, var(--korean-blue) 100%);
|
||||
}
|
||||
|
||||
/* Navigation Styles */
|
||||
.navbar {
|
||||
background: rgba(37, 99, 235, 0.95) !important;
|
||||
backdrop-filter: blur(10px);
|
||||
box-shadow: var(--shadow-md);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
font-family: var(--font-display);
|
||||
font-weight: 700;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
font-weight: 500;
|
||||
position: relative;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.nav-link:hover {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.nav-link.active::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -5px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 80%;
|
||||
height: 2px;
|
||||
background: var(--korean-red);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* Hero Section */
|
||||
.hero-section {
|
||||
background: linear-gradient(135deg, var(--primary-color) 0%, var(--korean-blue) 100%);
|
||||
min-height: 100vh;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.hero-background {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-image: url('/images/korea-bg.jpg');
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
opacity: 0.1;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.hero-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(135deg, rgba(37, 99, 235, 0.8) 0%, rgba(0, 52, 120, 0.9) 100%);
|
||||
z-index: 2;
|
||||
pointer-events: none; /* Позволяет кликам проходить через overlay */
|
||||
}
|
||||
|
||||
.hero-section .container {
|
||||
z-index: 3; /* Контент поверх overlay */
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-family: var(--font-display);
|
||||
font-size: clamp(2.5rem, 5vw, 4rem);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.hero-subtitle {
|
||||
font-size: clamp(1.1rem, 2vw, 1.3rem);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.hero-image-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.floating-card {
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
animation: float 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translateY(0px); }
|
||||
50% { transform: translateY(-10px); }
|
||||
}
|
||||
|
||||
.icon-circle {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
/* Search Section */
|
||||
.search-section {
|
||||
margin-top: -100px;
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.search-form .form-control,
|
||||
.search-form .form-select {
|
||||
border: 2px solid transparent;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.search-form .form-control:focus,
|
||||
.search-form .form-select:focus {
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 0.2rem rgba(37, 99, 235, 0.25);
|
||||
}
|
||||
|
||||
/* Section Titles */
|
||||
.section-title {
|
||||
font-family: var(--font-display);
|
||||
color: var(--dark-color);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.section-subtitle {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* Tour Cards */
|
||||
.tour-card {
|
||||
transition: all 0.3s ease;
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tour-card:hover {
|
||||
transform: translateY(-10px);
|
||||
box-shadow: var(--shadow-xl);
|
||||
}
|
||||
|
||||
.tour-image {
|
||||
height: 250px;
|
||||
object-fit: cover;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.tour-card:hover .tour-image {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.tour-overlay {
|
||||
background: linear-gradient(transparent, rgba(0, 0, 0, 0.6));
|
||||
opacity: 0;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.tour-card:hover .tour-overlay {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.tour-price .badge {
|
||||
font-size: 1rem;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.tour-type {
|
||||
top: 15px;
|
||||
right: 15px;
|
||||
}
|
||||
|
||||
.tour-meta {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* Article Cards */
|
||||
.article-card {
|
||||
transition: all 0.3s ease;
|
||||
border-radius: 15px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.article-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.article-image {
|
||||
height: 200px;
|
||||
object-fit: cover;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.article-card:hover .article-image {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
/* Stats Section */
|
||||
.stats-section {
|
||||
background: linear-gradient(135deg, var(--primary-color) 0%, var(--korean-blue) 100%);
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-family: var(--font-display);
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* Guide Cards */
|
||||
.guide-card {
|
||||
transition: all 0.3s ease;
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.guide-card:hover {
|
||||
transform: translateY(-10px);
|
||||
box-shadow: var(--shadow-xl);
|
||||
}
|
||||
|
||||
.guide-avatar {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
object-fit: cover;
|
||||
border-radius: 50%;
|
||||
border: 4px solid white;
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.guide-specialization {
|
||||
display: inline-block;
|
||||
padding: 0.25rem 0.75rem;
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
border-radius: 20px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.rating-stars {
|
||||
color: #fbbf24;
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
footer {
|
||||
background: linear-gradient(135deg, var(--gray-900) 0%, var(--dark-color) 100%);
|
||||
}
|
||||
|
||||
footer a {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
footer a:hover {
|
||||
color: var(--primary-light) !important;
|
||||
}
|
||||
|
||||
.social-links a {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 50%;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.social-links a:hover {
|
||||
background: var(--primary-color);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* Utility Classes */
|
||||
.rounded-4 {
|
||||
border-radius: 1.5rem !important;
|
||||
}
|
||||
|
||||
.rounded-pill {
|
||||
border-radius: 50rem !important;
|
||||
}
|
||||
|
||||
.shadow-soft {
|
||||
box-shadow: 0 2px 20px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
/* Loading States */
|
||||
.loading {
|
||||
position: relative;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.loading::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 2px solid transparent;
|
||||
border-top: 2px solid var(--primary-color);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: translate(-50%, -50%) rotate(0deg); }
|
||||
100% { transform: translate(-50%, -50%) rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
body {
|
||||
padding-top: 66px;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
||||
.hero-subtitle {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.floating-card {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.search-section {
|
||||
margin-top: -50px;
|
||||
}
|
||||
|
||||
.tour-card,
|
||||
.article-card,
|
||||
.guide-card {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.stats-section .stat-number {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.hero-buttons .btn {
|
||||
width: 100%;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.search-form .col-md-4 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.search-form .btn {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* Print Styles */
|
||||
@media print {
|
||||
.navbar,
|
||||
footer,
|
||||
.btn,
|
||||
.floating-card {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
body {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.hero-section {
|
||||
background: white;
|
||||
color: black;
|
||||
min-height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* High Contrast Mode */
|
||||
@media (prefers-contrast: high) {
|
||||
.tour-overlay,
|
||||
.hero-overlay {
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
}
|
||||
|
||||
.text-gradient {
|
||||
background: none;
|
||||
color: var(--dark-color);
|
||||
-webkit-text-fill-color: initial;
|
||||
}
|
||||
}
|
||||
|
||||
/* Reduced Motion */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
|
||||
.floating-card {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
BIN
public/images/city-tour-placeholder.webp
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
44
public/images/default-article.jpg
Normal file
@@ -0,0 +1,44 @@
|
||||
<!-- Korea Tourism Agency - Placeholder SVG Image Generator -->
|
||||
<svg width="600" height="400" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Gradient Background -->
|
||||
<defs>
|
||||
<linearGradient id="koreaGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#2563eb;stop-opacity:1" />
|
||||
<stop offset="50%" style="stop-color:#3b82f6;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#1e40af;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- Background -->
|
||||
<rect width="100%" height="100%" fill="url(#koreaGradient)"/>
|
||||
|
||||
<!-- Mountain Silhouette -->
|
||||
<path d="M0,300 Q150,200 300,250 Q450,200 600,280 L600,400 L0,400 Z" fill="rgba(255,255,255,0.1)"/>
|
||||
<path d="M0,320 Q100,240 200,280 Q400,220 600,300 L600,400 L0,400 Z" fill="rgba(255,255,255,0.05)"/>
|
||||
|
||||
<!-- Korean Flag Colors Accent -->
|
||||
<circle cx="500" cy="100" r="15" fill="#c41e3a" opacity="0.3"/>
|
||||
<circle cx="480" cy="120" r="10" fill="#003478" opacity="0.3"/>
|
||||
|
||||
<!-- Tour Bus Icon -->
|
||||
<g transform="translate(250,180)">
|
||||
<rect x="0" y="20" width="100" height="40" rx="5" fill="white" opacity="0.9"/>
|
||||
<rect x="10" y="25" width="15" height="12" fill="#2563eb"/>
|
||||
<rect x="30" y="25" width="15" height="12" fill="#2563eb"/>
|
||||
<rect x="50" y="25" width="15" height="12" fill="#2563eb"/>
|
||||
<rect x="70" y="25" width="15" height="12" fill="#2563eb"/>
|
||||
<circle cx="20" cy="65" r="8" fill="#374151"/>
|
||||
<circle cx="80" cy="65" r="8" fill="#374151"/>
|
||||
<rect x="0" y="45" width="100" height="8" fill="white" opacity="0.9"/>
|
||||
</g>
|
||||
|
||||
<!-- Korean Text -->
|
||||
<text x="300" y="330" font-family="Arial, sans-serif" font-size="24" font-weight="bold" text-anchor="middle" fill="white">한국 관광</text>
|
||||
<text x="300" y="350" font-family="Arial, sans-serif" font-size="16" text-anchor="middle" fill="white" opacity="0.8">Korea Tourism</text>
|
||||
|
||||
<!-- Decorative Elements -->
|
||||
<circle cx="50" cy="50" r="3" fill="white" opacity="0.4"/>
|
||||
<circle cx="550" cy="80" r="2" fill="white" opacity="0.6"/>
|
||||
<circle cx="100" cy="120" r="2" fill="white" opacity="0.5"/>
|
||||
<circle cx="520" cy="200" r="3" fill="white" opacity="0.3"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
44
public/images/default-tour.jpg
Normal file
@@ -0,0 +1,44 @@
|
||||
<!-- Korea Tourism Agency - Placeholder SVG Image Generator -->
|
||||
<svg width="600" height="400" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Gradient Background -->
|
||||
<defs>
|
||||
<linearGradient id="koreaGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#2563eb;stop-opacity:1" />
|
||||
<stop offset="50%" style="stop-color:#3b82f6;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#1e40af;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- Background -->
|
||||
<rect width="100%" height="100%" fill="url(#koreaGradient)"/>
|
||||
|
||||
<!-- Mountain Silhouette -->
|
||||
<path d="M0,300 Q150,200 300,250 Q450,200 600,280 L600,400 L0,400 Z" fill="rgba(255,255,255,0.1)"/>
|
||||
<path d="M0,320 Q100,240 200,280 Q400,220 600,300 L600,400 L0,400 Z" fill="rgba(255,255,255,0.05)"/>
|
||||
|
||||
<!-- Korean Flag Colors Accent -->
|
||||
<circle cx="500" cy="100" r="15" fill="#c41e3a" opacity="0.3"/>
|
||||
<circle cx="480" cy="120" r="10" fill="#003478" opacity="0.3"/>
|
||||
|
||||
<!-- Tour Bus Icon -->
|
||||
<g transform="translate(250,180)">
|
||||
<rect x="0" y="20" width="100" height="40" rx="5" fill="white" opacity="0.9"/>
|
||||
<rect x="10" y="25" width="15" height="12" fill="#2563eb"/>
|
||||
<rect x="30" y="25" width="15" height="12" fill="#2563eb"/>
|
||||
<rect x="50" y="25" width="15" height="12" fill="#2563eb"/>
|
||||
<rect x="70" y="25" width="15" height="12" fill="#2563eb"/>
|
||||
<circle cx="20" cy="65" r="8" fill="#374151"/>
|
||||
<circle cx="80" cy="65" r="8" fill="#374151"/>
|
||||
<rect x="0" y="45" width="100" height="8" fill="white" opacity="0.9"/>
|
||||
</g>
|
||||
|
||||
<!-- Korean Text -->
|
||||
<text x="300" y="330" font-family="Arial, sans-serif" font-size="24" font-weight="bold" text-anchor="middle" fill="white">한국 관광</text>
|
||||
<text x="300" y="350" font-family="Arial, sans-serif" font-size="16" text-anchor="middle" fill="white" opacity="0.8">Korea Tourism</text>
|
||||
|
||||
<!-- Decorative Elements -->
|
||||
<circle cx="50" cy="50" r="3" fill="white" opacity="0.4"/>
|
||||
<circle cx="550" cy="80" r="2" fill="white" opacity="0.6"/>
|
||||
<circle cx="100" cy="120" r="2" fill="white" opacity="0.5"/>
|
||||
<circle cx="520" cy="200" r="3" fill="white" opacity="0.3"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
BIN
public/images/fish-placeholder.jpg
Normal file
|
After Width: | Height: | Size: 8.2 KiB |
44
public/images/korea-hero.jpg
Normal file
@@ -0,0 +1,44 @@
|
||||
<!-- Korea Tourism Agency - Placeholder SVG Image Generator -->
|
||||
<svg width="600" height="400" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Gradient Background -->
|
||||
<defs>
|
||||
<linearGradient id="koreaGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#2563eb;stop-opacity:1" />
|
||||
<stop offset="50%" style="stop-color:#3b82f6;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#1e40af;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- Background -->
|
||||
<rect width="100%" height="100%" fill="url(#koreaGradient)"/>
|
||||
|
||||
<!-- Mountain Silhouette -->
|
||||
<path d="M0,300 Q150,200 300,250 Q450,200 600,280 L600,400 L0,400 Z" fill="rgba(255,255,255,0.1)"/>
|
||||
<path d="M0,320 Q100,240 200,280 Q400,220 600,300 L600,400 L0,400 Z" fill="rgba(255,255,255,0.05)"/>
|
||||
|
||||
<!-- Korean Flag Colors Accent -->
|
||||
<circle cx="500" cy="100" r="15" fill="#c41e3a" opacity="0.3"/>
|
||||
<circle cx="480" cy="120" r="10" fill="#003478" opacity="0.3"/>
|
||||
|
||||
<!-- Tour Bus Icon -->
|
||||
<g transform="translate(250,180)">
|
||||
<rect x="0" y="20" width="100" height="40" rx="5" fill="white" opacity="0.9"/>
|
||||
<rect x="10" y="25" width="15" height="12" fill="#2563eb"/>
|
||||
<rect x="30" y="25" width="15" height="12" fill="#2563eb"/>
|
||||
<rect x="50" y="25" width="15" height="12" fill="#2563eb"/>
|
||||
<rect x="70" y="25" width="15" height="12" fill="#2563eb"/>
|
||||
<circle cx="20" cy="65" r="8" fill="#374151"/>
|
||||
<circle cx="80" cy="65" r="8" fill="#374151"/>
|
||||
<rect x="0" y="45" width="100" height="8" fill="white" opacity="0.9"/>
|
||||
</g>
|
||||
|
||||
<!-- Korean Text -->
|
||||
<text x="300" y="330" font-family="Arial, sans-serif" font-size="24" font-weight="bold" text-anchor="middle" fill="white">한국 관광</text>
|
||||
<text x="300" y="350" font-family="Arial, sans-serif" font-size="16" text-anchor="middle" fill="white" opacity="0.8">Korea Tourism</text>
|
||||
|
||||
<!-- Decorative Elements -->
|
||||
<circle cx="50" cy="50" r="3" fill="white" opacity="0.4"/>
|
||||
<circle cx="550" cy="80" r="2" fill="white" opacity="0.6"/>
|
||||
<circle cx="100" cy="120" r="2" fill="white" opacity="0.5"/>
|
||||
<circle cx="520" cy="200" r="3" fill="white" opacity="0.3"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
BIN
public/images/mountain-placeholder.jpg
Normal file
|
After Width: | Height: | Size: 78 KiB |
BIN
public/images/placeholder.jpg
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
43
public/images/placeholders/default-guide.svg
Normal file
@@ -0,0 +1,43 @@
|
||||
<!-- Korea Tourism Agency - Guide Placeholder SVG -->
|
||||
<svg width="300" height="300" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Gradient Background -->
|
||||
<defs>
|
||||
<linearGradient id="guideGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#f3f4f6;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#e5e7eb;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- Background -->
|
||||
<rect width="100%" height="100%" fill="url(#guideGradient)"/>
|
||||
|
||||
<!-- Guide Icon -->
|
||||
<g transform="translate(150,150)">
|
||||
<!-- Head -->
|
||||
<circle cx="0" cy="-40" r="25" fill="#2563eb"/>
|
||||
|
||||
<!-- Body -->
|
||||
<rect x="-20" y="-15" width="40" height="60" rx="10" fill="#3b82f6"/>
|
||||
|
||||
<!-- Arms -->
|
||||
<rect x="-30" y="-10" width="15" height="40" rx="7" fill="#2563eb"/>
|
||||
<rect x="15" y="-10" width="15" height="40" rx="7" fill="#2563eb"/>
|
||||
|
||||
<!-- Legs -->
|
||||
<rect x="-15" y="45" width="12" height="30" rx="6" fill="#1e40af"/>
|
||||
<rect x="3" y="45" width="12" height="30" rx="6" fill="#1e40af"/>
|
||||
|
||||
<!-- Korean Flag Badge -->
|
||||
<rect x="8" y="5" width="12" height="8" fill="#c41e3a"/>
|
||||
<rect x="8" y="5" width="6" height="4" fill="white"/>
|
||||
<rect x="8" y="9" width="6" height="4" fill="#003478"/>
|
||||
|
||||
<!-- Guide Flag -->
|
||||
<rect x="25" y="-25" width="3" height="40" fill="#8b5cf6"/>
|
||||
<rect x="28" y="-25" width="20" height="12" fill="#a855f7"/>
|
||||
</g>
|
||||
|
||||
<!-- Korean Text -->
|
||||
<text x="150" y="250" font-family="Arial, sans-serif" font-size="18" font-weight="bold" text-anchor="middle" fill="#374151">가이드</text>
|
||||
<text x="150" y="270" font-family="Arial, sans-serif" font-size="14" text-anchor="middle" fill="#6b7280">Professional Guide</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
44
public/images/placeholders/default-tour.svg
Normal file
@@ -0,0 +1,44 @@
|
||||
<!-- Korea Tourism Agency - Placeholder SVG Image Generator -->
|
||||
<svg width="600" height="400" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Gradient Background -->
|
||||
<defs>
|
||||
<linearGradient id="koreaGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#2563eb;stop-opacity:1" />
|
||||
<stop offset="50%" style="stop-color:#3b82f6;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#1e40af;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- Background -->
|
||||
<rect width="100%" height="100%" fill="url(#koreaGradient)"/>
|
||||
|
||||
<!-- Mountain Silhouette -->
|
||||
<path d="M0,300 Q150,200 300,250 Q450,200 600,280 L600,400 L0,400 Z" fill="rgba(255,255,255,0.1)"/>
|
||||
<path d="M0,320 Q100,240 200,280 Q400,220 600,300 L600,400 L0,400 Z" fill="rgba(255,255,255,0.05)"/>
|
||||
|
||||
<!-- Korean Flag Colors Accent -->
|
||||
<circle cx="500" cy="100" r="15" fill="#c41e3a" opacity="0.3"/>
|
||||
<circle cx="480" cy="120" r="10" fill="#003478" opacity="0.3"/>
|
||||
|
||||
<!-- Tour Bus Icon -->
|
||||
<g transform="translate(250,180)">
|
||||
<rect x="0" y="20" width="100" height="40" rx="5" fill="white" opacity="0.9"/>
|
||||
<rect x="10" y="25" width="15" height="12" fill="#2563eb"/>
|
||||
<rect x="30" y="25" width="15" height="12" fill="#2563eb"/>
|
||||
<rect x="50" y="25" width="15" height="12" fill="#2563eb"/>
|
||||
<rect x="70" y="25" width="15" height="12" fill="#2563eb"/>
|
||||
<circle cx="20" cy="65" r="8" fill="#374151"/>
|
||||
<circle cx="80" cy="65" r="8" fill="#374151"/>
|
||||
<rect x="0" y="45" width="100" height="8" fill="white" opacity="0.9"/>
|
||||
</g>
|
||||
|
||||
<!-- Korean Text -->
|
||||
<text x="300" y="330" font-family="Arial, sans-serif" font-size="24" font-weight="bold" text-anchor="middle" fill="white">한국 관광</text>
|
||||
<text x="300" y="350" font-family="Arial, sans-serif" font-size="16" text-anchor="middle" fill="white" opacity="0.8">Korea Tourism</text>
|
||||
|
||||
<!-- Decorative Elements -->
|
||||
<circle cx="50" cy="50" r="3" fill="white" opacity="0.4"/>
|
||||
<circle cx="550" cy="80" r="2" fill="white" opacity="0.6"/>
|
||||
<circle cx="100" cy="120" r="2" fill="white" opacity="0.5"/>
|
||||
<circle cx="520" cy="200" r="3" fill="white" opacity="0.3"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
288
public/js/admin-custom.js
Normal file
@@ -0,0 +1,288 @@
|
||||
/* Korea Tourism Agency Admin Panel Custom Scripts */
|
||||
|
||||
$(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);
|
||||
|
||||
// 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">×</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();
|
||||
}
|
||||
};
|
||||
391
public/js/main.js
Normal file
@@ -0,0 +1,391 @@
|
||||
/**
|
||||
* Korea Tourism Agency - Main JavaScript
|
||||
* Основные интерактивные функции для сайта
|
||||
*/
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
// ==========================================
|
||||
// Инициализация AOS (Animate On Scroll)
|
||||
// ==========================================
|
||||
if (typeof AOS !== 'undefined') {
|
||||
AOS.init({
|
||||
duration: 800,
|
||||
easing: 'ease-in-out',
|
||||
once: true,
|
||||
offset: 100
|
||||
});
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Навигация и мобильное меню
|
||||
// ==========================================
|
||||
const navbar = document.querySelector('.navbar');
|
||||
|
||||
// Добавление класса при скролле
|
||||
window.addEventListener('scroll', function() {
|
||||
if (window.scrollY > 100) {
|
||||
if (navbar) navbar.classList.add('scrolled');
|
||||
} else {
|
||||
if (navbar) navbar.classList.remove('scrolled');
|
||||
}
|
||||
});
|
||||
|
||||
// ==========================================
|
||||
// Поиск по сайту
|
||||
// ==========================================
|
||||
const searchInput = document.getElementById('search-input');
|
||||
const searchResults = document.getElementById('search-results');
|
||||
|
||||
if (searchInput && searchResults) {
|
||||
let searchTimeout;
|
||||
|
||||
searchInput.addEventListener('input', function() {
|
||||
const query = this.value.trim();
|
||||
|
||||
clearTimeout(searchTimeout);
|
||||
|
||||
if (query.length < 2) {
|
||||
searchResults.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
searchTimeout = setTimeout(() => {
|
||||
performSearch(query);
|
||||
}, 300);
|
||||
});
|
||||
|
||||
// Скрытие результатов при клике вне поиска
|
||||
document.addEventListener('click', function(e) {
|
||||
if (!searchInput.contains(e.target) && !searchResults.contains(e.target)) {
|
||||
searchResults.style.display = 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function performSearch(query) {
|
||||
try {
|
||||
const response = await fetch('/api/search?q=' + encodeURIComponent(query));
|
||||
const data = await response.json();
|
||||
|
||||
displaySearchResults(data);
|
||||
} catch (error) {
|
||||
console.error('Search error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function displaySearchResults(results) {
|
||||
if (!searchResults) return;
|
||||
|
||||
let html = '';
|
||||
|
||||
if (results.routes && results.routes.length > 0) {
|
||||
html += '<div class="search-category"><h6>투어</h6>';
|
||||
results.routes.forEach(route => {
|
||||
html += '<div class="search-item">';
|
||||
html += '<a href="/routes/' + route.id + '">';
|
||||
html += '<strong>' + (route.name_ko || route.name_en) + '</strong>';
|
||||
html += '<small class="text-muted d-block">' + route.location + '</small>';
|
||||
html += '</a></div>';
|
||||
});
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
if (results.guides && results.guides.length > 0) {
|
||||
html += '<div class="search-category"><h6>가이드</h6>';
|
||||
results.guides.forEach(guide => {
|
||||
html += '<div class="search-item">';
|
||||
html += '<a href="/guides/' + guide.id + '">';
|
||||
html += '<strong>' + guide.name + '</strong>';
|
||||
html += '<small class="text-muted d-block">' + guide.specialization + '</small>';
|
||||
html += '</a></div>';
|
||||
});
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
if (results.articles && results.articles.length > 0) {
|
||||
html += '<div class="search-category"><h6>기사</h6>';
|
||||
results.articles.forEach(article => {
|
||||
html += '<div class="search-item">';
|
||||
html += '<a href="/articles/' + article.id + '">';
|
||||
html += '<strong>' + (article.title_ko || article.title_en) + '</strong>';
|
||||
html += '</a></div>';
|
||||
});
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
if (!html) {
|
||||
html = '<div class="search-item text-muted">검색 결과가 없습니다</div>';
|
||||
}
|
||||
|
||||
searchResults.innerHTML = html;
|
||||
searchResults.style.display = 'block';
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Фильтрация туров
|
||||
// ==========================================
|
||||
const routeFilters = document.querySelectorAll('.route-filter');
|
||||
const routeCards = document.querySelectorAll('.route-card');
|
||||
|
||||
routeFilters.forEach(filter => {
|
||||
filter.addEventListener('click', function() {
|
||||
const category = this.dataset.category;
|
||||
|
||||
// Обновление активного фильтра
|
||||
routeFilters.forEach(f => f.classList.remove('active'));
|
||||
this.classList.add('active');
|
||||
|
||||
// Фильтрация карточек
|
||||
routeCards.forEach(card => {
|
||||
if (category === 'all' || card.dataset.category === category) {
|
||||
card.style.display = 'block';
|
||||
card.classList.add('fade-in');
|
||||
} else {
|
||||
card.style.display = 'none';
|
||||
card.classList.remove('fade-in');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ==========================================
|
||||
// Форма бронирования
|
||||
// ==========================================
|
||||
const bookingForm = document.getElementById('booking-form');
|
||||
|
||||
if (bookingForm) {
|
||||
bookingForm.addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const submitBtn = this.querySelector('button[type="submit"]');
|
||||
const originalText = submitBtn.textContent;
|
||||
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 전송 중...';
|
||||
|
||||
try {
|
||||
const formData = new FormData(this);
|
||||
const response = await fetch('/api/booking', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
showAlert('success', '예약 요청이 성공적으로 전송되었습니다!');
|
||||
this.reset();
|
||||
} else {
|
||||
showAlert('danger', result.error || '오류가 발생했습니다.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Booking error:', error);
|
||||
showAlert('danger', '네트워크 오류가 발생했습니다.');
|
||||
} finally {
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.textContent = originalText;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Форма контактов
|
||||
// ==========================================
|
||||
const contactForm = document.getElementById('contact-form');
|
||||
|
||||
if (contactForm) {
|
||||
contactForm.addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const submitBtn = this.querySelector('button[type="submit"]');
|
||||
const originalText = submitBtn.textContent;
|
||||
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 전송 중...';
|
||||
|
||||
try {
|
||||
const formData = new FormData(this);
|
||||
const response = await fetch('/api/contact', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
showAlert('success', '메시지가 성공적으로 전송되었습니다!');
|
||||
this.reset();
|
||||
} else {
|
||||
showAlert('danger', result.error || '오류가 발생했습니다.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Contact error:', error);
|
||||
showAlert('danger', '네트워크 오류가 발생했습니다.');
|
||||
} finally {
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.textContent = originalText;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Галерея изображений
|
||||
// ==========================================
|
||||
const galleryItems = document.querySelectorAll('.gallery-item');
|
||||
|
||||
galleryItems.forEach(item => {
|
||||
item.addEventListener('click', function() {
|
||||
const img = this.querySelector('img');
|
||||
if (img) {
|
||||
showImageModal(img.src, img.alt || 'Gallery Image');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function showImageModal(src, alt) {
|
||||
const modal = document.createElement('div');
|
||||
modal.className = 'image-modal';
|
||||
modal.innerHTML =
|
||||
'<div class="image-modal-backdrop">' +
|
||||
'<div class="image-modal-content">' +
|
||||
'<button class="image-modal-close" type="button">' +
|
||||
'<i class="fas fa-times"></i>' +
|
||||
'</button>' +
|
||||
'<img src="' + src + '" alt="' + alt + '" class="img-fluid">' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
|
||||
document.body.appendChild(modal);
|
||||
setTimeout(() => modal.classList.add('show'), 10);
|
||||
|
||||
// Закрытие модального окна
|
||||
const closeModal = () => {
|
||||
modal.classList.remove('show');
|
||||
setTimeout(() => {
|
||||
if (modal.parentNode) {
|
||||
document.body.removeChild(modal);
|
||||
}
|
||||
}, 300);
|
||||
};
|
||||
|
||||
modal.querySelector('.image-modal-close').addEventListener('click', closeModal);
|
||||
modal.querySelector('.image-modal-backdrop').addEventListener('click', function(e) {
|
||||
if (e.target === this) closeModal();
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') closeModal();
|
||||
});
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Плавная прокрутка к секциям
|
||||
// ==========================================
|
||||
const scrollLinks = document.querySelectorAll('a[href^="#"]');
|
||||
|
||||
scrollLinks.forEach(link => {
|
||||
link.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const targetId = this.getAttribute('href').substring(1);
|
||||
const targetElement = document.getElementById(targetId);
|
||||
|
||||
if (targetElement) {
|
||||
targetElement.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start'
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ==========================================
|
||||
// Валидация форм
|
||||
// ==========================================
|
||||
const forms = document.querySelectorAll('.needs-validation');
|
||||
|
||||
forms.forEach(form => {
|
||||
form.addEventListener('submit', function(e) {
|
||||
if (!form.checkValidity()) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
form.classList.add('was-validated');
|
||||
});
|
||||
});
|
||||
|
||||
// ==========================================
|
||||
// Tooltips и Popovers
|
||||
// ==========================================
|
||||
if (typeof bootstrap !== 'undefined') {
|
||||
const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
|
||||
tooltipTriggerList.map(function (tooltipTriggerEl) {
|
||||
return new bootstrap.Tooltip(tooltipTriggerEl);
|
||||
});
|
||||
|
||||
const popoverTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]'));
|
||||
popoverTriggerList.map(function (popoverTriggerEl) {
|
||||
return new bootstrap.Popover(popoverTriggerEl);
|
||||
});
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Lazy loading изображений
|
||||
// ==========================================
|
||||
const lazyImages = document.querySelectorAll('img[data-src]');
|
||||
|
||||
if ('IntersectionObserver' in window) {
|
||||
const imageObserver = new IntersectionObserver((entries, observer) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
const img = entry.target;
|
||||
img.src = img.dataset.src;
|
||||
img.classList.remove('lazy');
|
||||
imageObserver.unobserve(img);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
lazyImages.forEach(img => imageObserver.observe(img));
|
||||
} else {
|
||||
// Fallback для старых браузеров
|
||||
lazyImages.forEach(img => {
|
||||
img.src = img.dataset.src;
|
||||
img.classList.remove('lazy');
|
||||
});
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Утилитарные функции
|
||||
// ==========================================
|
||||
function showAlert(type, message) {
|
||||
const alertContainer = document.getElementById('alert-container') || createAlertContainer();
|
||||
|
||||
const alert = document.createElement('div');
|
||||
alert.className = 'alert alert-' + type + ' alert-dismissible fade show';
|
||||
alert.innerHTML =
|
||||
message +
|
||||
'<button type="button" class="btn-close" data-bs-dismiss="alert"></button>';
|
||||
|
||||
alertContainer.appendChild(alert);
|
||||
|
||||
// Автоскрытие через 5 секунд
|
||||
setTimeout(() => {
|
||||
if (alert.parentNode) {
|
||||
alert.remove();
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
function createAlertContainer() {
|
||||
const container = document.createElement('div');
|
||||
container.id = 'alert-container';
|
||||
container.className = 'position-fixed top-0 end-0 p-3';
|
||||
container.style.zIndex = '9999';
|
||||
document.body.appendChild(container);
|
||||
return container;
|
||||
}
|
||||
|
||||
console.log('Korea Tourism Agency - JavaScript loaded successfully! 🇰🇷');
|
||||
});
|
||||