прибрался
This commit is contained in:
1170
public/css/style.css
Normal file
1170
public/css/style.css
Normal file
File diff suppressed because it is too large
Load Diff
1307
public/index.html
1307
public/index.html
File diff suppressed because it is too large
Load Diff
148
public/js/main.js
Normal file
148
public/js/main.js
Normal file
@@ -0,0 +1,148 @@
|
||||
// Preloader
|
||||
window.addEventListener('load', () => {
|
||||
setTimeout(() => {
|
||||
document.getElementById('preloader').classList.add('hidden');
|
||||
}, 1500);
|
||||
});
|
||||
|
||||
// Navbar scroll effect
|
||||
const navbar = document.querySelector('.navbar');
|
||||
window.addEventListener('scroll', () => {
|
||||
if (window.scrollY > 80) {
|
||||
navbar.classList.add('scrolled');
|
||||
} else {
|
||||
navbar.classList.remove('scrolled');
|
||||
}
|
||||
});
|
||||
|
||||
// Scroll animations
|
||||
const observerOptions = {
|
||||
threshold: 0.1,
|
||||
rootMargin: '0px 0px -50px 0px'
|
||||
};
|
||||
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
entry.target.classList.add('animated');
|
||||
}
|
||||
});
|
||||
}, observerOptions);
|
||||
|
||||
document.querySelectorAll('.animate-on-scroll').forEach(el => {
|
||||
observer.observe(el);
|
||||
});
|
||||
|
||||
// Counter animation
|
||||
const counterObserver = new IntersectionObserver((entries) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
const counters = entry.target.querySelectorAll('.counter');
|
||||
counters.forEach(counter => {
|
||||
const target = parseInt(counter.getAttribute('data-target'));
|
||||
const duration = 2000;
|
||||
const step = target / (duration / 16);
|
||||
let current = 0;
|
||||
const timer = setInterval(() => {
|
||||
current += step;
|
||||
if (current >= target) {
|
||||
counter.textContent = target;
|
||||
clearInterval(timer);
|
||||
} else {
|
||||
counter.textContent = Math.floor(current);
|
||||
}
|
||||
}, 16);
|
||||
});
|
||||
counterObserver.unobserve(entry.target);
|
||||
}
|
||||
});
|
||||
}, { threshold: 0.5 });
|
||||
|
||||
const statsSection = document.querySelector('.hero-stats');
|
||||
if (statsSection) counterObserver.observe(statsSection);
|
||||
|
||||
// Booking modal - set room name
|
||||
document.querySelectorAll('.btn-book').forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
const room = this.getAttribute('data-room');
|
||||
document.getElementById('selectedRoom').value = room;
|
||||
});
|
||||
});
|
||||
|
||||
// Form submission
|
||||
document.getElementById('bookingForm').addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
const btn = this.querySelector('.btn-submit-booking');
|
||||
const originalText = btn.innerHTML;
|
||||
|
||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Отправка...';
|
||||
btn.disabled = true;
|
||||
|
||||
const formData = {
|
||||
name: this.querySelector('[name="name"]').value,
|
||||
phone: this.querySelector('[name="phone"]').value,
|
||||
adults: parseInt(this.querySelector('[name="guests"]').value),
|
||||
children: 0,
|
||||
checkin: this.querySelector('[name="checkin"]').value,
|
||||
checkout: this.querySelector('[name="checkout"]').value,
|
||||
wishes: this.querySelector('[name="wishes"]').value,
|
||||
room: document.getElementById('selectedRoom').value
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/bookings', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(formData)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
btn.innerHTML = '<i class="fas fa-check me-2"></i>Заявка отправлена Рауфу Алексеевичу!';
|
||||
btn.style.background = '#25d366';
|
||||
this.reset();
|
||||
setTimeout(() => {
|
||||
btn.innerHTML = originalText;
|
||||
btn.style.background = '';
|
||||
btn.disabled = false;
|
||||
bootstrap.Modal.getInstance(document.getElementById('bookingModal')).hide();
|
||||
}, 2500);
|
||||
} else {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.error || 'Ошибка сервера');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Booking error:', error);
|
||||
btn.innerHTML = '<i class="fas fa-exclamation-triangle me-2"></i>Ошибка отправки';
|
||||
btn.style.background = '#c9302c';
|
||||
setTimeout(() => {
|
||||
btn.innerHTML = originalText;
|
||||
btn.style.background = '';
|
||||
btn.disabled = false;
|
||||
}, 3000);
|
||||
}
|
||||
});
|
||||
|
||||
// Smooth scroll for nav links
|
||||
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
||||
anchor.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
const target = document.querySelector(this.getAttribute('href'));
|
||||
if (target) {
|
||||
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
// Close mobile menu
|
||||
const navCollapse = document.querySelector('.navbar-collapse');
|
||||
if (navCollapse.classList.contains('show')) {
|
||||
bootstrap.Collapse.getInstance(navCollapse).hide();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Parallax effect on hero
|
||||
window.addEventListener('scroll', () => {
|
||||
const hero = document.querySelector('.hero-bg');
|
||||
if (hero) {
|
||||
const scrolled = window.scrollY;
|
||||
hero.style.transform = `scale(${1 + scrolled * 0.0001}) translateY(${scrolled * 0.3}px)`;
|
||||
}
|
||||
});
|
||||
21
server.js
21
server.js
@@ -31,8 +31,17 @@ db.run(`CREATE TABLE IF NOT EXISTS bookings (
|
||||
children INTEGER NOT NULL,
|
||||
checkin_date TEXT NOT NULL,
|
||||
checkout_date TEXT NOT NULL,
|
||||
wishes TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)`);
|
||||
)`, (err) => {
|
||||
if (err) console.error('Table creation error:', err);
|
||||
});
|
||||
|
||||
db.run(`ALTER TABLE bookings ADD COLUMN wishes TEXT`, (err) => {
|
||||
if (err && !err.message.includes('duplicate column name')) {
|
||||
console.error('Migration error:', err);
|
||||
}
|
||||
});
|
||||
|
||||
// Image conversion (automatically convert JPEG/PNG to WebP)
|
||||
async function convertImages() {
|
||||
@@ -63,13 +72,13 @@ async function convertImages() {
|
||||
|
||||
// API: POST /api/bookings – сохранить новую заявку
|
||||
app.post('/api/bookings', (req, res) => {
|
||||
const { name, phone, adults, children, checkin, checkout } = req.body;
|
||||
const { name, phone, adults, children, checkin, checkout, wishes } = req.body;
|
||||
if (!name || !phone || !adults || !checkin || !checkout) {
|
||||
return res.status(400).json({ error: 'Missing required fields' });
|
||||
}
|
||||
const stmt = db.prepare(`INSERT INTO bookings (name, phone, adults, children, checkin_date, checkout_date)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`);
|
||||
stmt.run(name, phone, adults, children || 0, checkin, checkout, function(err) {
|
||||
const stmt = db.prepare(`INSERT INTO bookings (name, phone, adults, children, checkin_date, checkout_date, wishes)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)`);
|
||||
stmt.run(name, phone, parseInt(adults), parseInt(children || 0), checkin, checkout, wishes || null, function(err) {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
return res.status(500).json({ error: 'Database error' });
|
||||
@@ -85,7 +94,7 @@ app.get('/api/bookings', (req, res) => {
|
||||
if (!providedKey || providedKey !== API_KEY) {
|
||||
return res.status(401).json({ error: 'Invalid or missing API key' });
|
||||
}
|
||||
db.all(`SELECT id, name, phone, adults, children, checkin_date, checkout_date, created_at
|
||||
db.all(`SELECT id, name, phone, adults, children, checkin_date, checkout_date, wishes, created_at
|
||||
FROM bookings ORDER BY created_at DESC`, (err, rows) => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
|
||||
Reference in New Issue
Block a user