This commit is contained in:
2026-05-03 12:56:41 +05:00
parent 2e6d692ad4
commit 5aba7681ce
6 changed files with 157 additions and 28 deletions

3
.gitignore vendored
View File

@@ -1,2 +1,3 @@
node_modules
package-lock.json
package-lock.json
.env

BIN
data/bookings.db Normal file

Binary file not shown.

View File

@@ -1,6 +1,8 @@
{
"dependencies": {
"dotenv": "^17.4.2",
"express": "^5.2.1",
"sharp": "^0.34.5"
"sharp": "^0.34.5",
"sqlite3": "^6.0.1"
}
}

View File

@@ -78,11 +78,28 @@
<label for="phone" data-i18n="label_phone">Номер телефона</label>
<input type="tel" id="phone" name="phone" required placeholder="+7 (___) ___-__-__">
</div>
<div class="form-group">
<label for="adults" data-i18n="book_adults">Количество взрослых</label>
<input type="number" id="adults" name="adults" min="1" value="1" required>
</div>
<div class="form-group">
<label for="children" data-i18n="book_children">Количество детей (до 12 лет)</label>
<input type="number" id="children" name="children" min="0" value="0">
</div>
<div class="form-group">
<label for="checkin" data-i18n="book_checkin">Дата заезда</label>
<input type="date" id="checkin" name="checkin" required>
</div>
<div class="form-group">
<label for="checkout" data-i18n="book_checkout">Дата выезда</label>
<input type="date" id="checkout" name="checkout" required>
</div>
<div style="margin:1.5rem 0; font-size:0.9rem; color:#666;">
<input type="checkbox" id="consent" required style="margin-right:8px;">
<label for="consent" data-i18n="fz152" style="display:inline; font-weight:400;">Согласие на обработку данных (152-ФЗ)</label>
</div>
<button type="submit" class="btn full-width" data-i18n="book_btn">Отправить заявку</button>
<div id="bookingMessage" style="margin-top:1rem; text-align:center;"></div>
</form>
</section>

View File

@@ -1,4 +1,4 @@
// Глобальные переводы (расширены для location, about и фактов о городах)
// Глобальные переводы (расширены для location, about, booking)
window.translations = {
ru: {
nav_about: "О нас",
@@ -41,6 +41,14 @@ window.translations = {
loc_city_primorsk: "Приморск",
loc_city_gudauta: "Гудаута",
loc_km: "км",
// Новые поля для бронирования
book_adults: "Количество взрослых",
book_children: "Количество детей (до 12 лет)",
book_checkin: "Дата заезда",
book_checkout: "Дата выезда",
please_fill_all: "Пожалуйста, заполните все обязательные поля",
booking_success: "Заявка отправлена! Мы свяжемся с вами.",
booking_error: "Ошибка отправки. Попробуйте позже.",
// Факты о городах (русский)
loc_facts_sukhum: [
"🏛️ Один из древнейших городов мира, основан в VI веке до н.э.",
@@ -120,7 +128,15 @@ window.translations = {
loc_city_primorsk: "Primorsk",
loc_city_gudauta: "Gudauta",
loc_km: "km",
// Facts about cities (English)
// New booking fields
book_adults: "Number of adults",
book_children: "Number of children (under 12)",
book_checkin: "Check-in date",
book_checkout: "Check-out date",
please_fill_all: "Please fill all required fields",
booking_success: "Request sent! We will contact you.",
booking_error: "Submission error. Please try again later.",
// City facts (English)
loc_facts_sukhum: [
"🏛️ One of the oldest cities in the world, founded in the 6th century BC.",
"🌿 Botanical Garden one of the oldest in the Caucasus (founded 1838).",
@@ -199,7 +215,15 @@ window.translations = {
loc_city_primorsk: "Приморск",
loc_city_gudauta: "Гәдоуҭа",
loc_km: "км",
// Факты об городах на абхазском (упрощённо, для демонстрации)
// New booking fields in Abkhaz
book_adults: "Аҧшәмаҭааҩцәа рхыҧхьаӡара",
book_children: "Аҵлақәа рхыҧхьаӡара (12 шықәса рҟынӡа)",
book_checkin: "Аҭагалара амш",
book_checkout: "Акәылара амш",
please_fill_all: "Ишәҧшәа, азықәҭа змоу аҭыԥқәа рымч",
booking_success: "Азаявка ацәыҵит! Ҳара шәыҟазаалак шәааҽазыр.",
booking_error: "Ацәыҵра аҟаҵаразы алшара. Ушәа агәаҧш, ушәа шәааҽаз.",
// City facts Abkhaz (simplified)
loc_facts_sukhum: [
"🏛️ Адунеи аиҳабылакьықәа руакы, VI ашәышықәса рахь нҵа иҟоуп.",
"🌿 Аботаникатә сад — Акавказ аиҳабылакьықәа руакы (1838 ш.).",
@@ -218,7 +242,7 @@ window.translations = {
loc_facts_gal: [
"🌾 Аԥсны аҳәынҭқарратә аԥштәыҩсатә центр.",
"🍈 Иҭоуҳәоуп аҵәа, арбузқәа.",
"🏛️ Амингрел ҳәынҭқарратә культура (америкатәи америкаҭтәи америкатәи америкаҭ) иааҵанакуеит."
"🏛️ Амингрел ҳәынҭқарратә культура иааҵанакуеит."
],
loc_facts_new_athos: [
"⛪ Иҭоуҳәоуп Афон Ҵыцтәи ауахәама (XIX ашә.).",
@@ -306,12 +330,46 @@ document.addEventListener("DOMContentLoaded", () => {
setTimeout(() => document.getElementById('cookieBanner').classList.add('show'), 2000);
}
// Форма бронирования
// Форма бронирования с отправкой на сервер
const bookingForm = document.getElementById('bookingForm');
if (bookingForm) {
bookingForm.onsubmit = (e) => {
bookingForm.onsubmit = async (e) => {
e.preventDefault();
alert(window.translations[localStorage.getItem('siteLang') || 'ru'].alert_msg);
const lang = localStorage.getItem('siteLang') || 'ru';
const name = document.getElementById('name').value.trim();
const phone = document.getElementById('phone').value.trim();
const adults = parseInt(document.getElementById('adults').value);
const children = parseInt(document.getElementById('children').value) || 0;
const checkin = document.getElementById('checkin').value;
const checkout = document.getElementById('checkout').value;
const consent = document.getElementById('consent').checked;
if (!name || !phone || !adults || !checkin || !checkout || !consent) {
alert(window.translations[lang].please_fill_all);
return;
}
const payload = { name, phone, adults, children, checkin, checkout };
try {
const response = await fetch('/api/bookings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const result = await response.json();
const msgDiv = document.getElementById('bookingMessage');
if (response.ok) {
msgDiv.innerHTML = `<div style="color: green; background:#e0f2e9; padding:0.8rem; border-radius:12px;">${window.translations[lang].booking_success}</div>`;
bookingForm.reset();
document.getElementById('adults').value = '1';
document.getElementById('children').value = '0';
} else {
msgDiv.innerHTML = `<div style="color: red;">${result.error || window.translations[lang].booking_error}</div>`;
}
} catch (err) {
document.getElementById('bookingMessage').innerHTML = `<div style="color: red;">${window.translations[lang].booking_error}</div>`;
}
};
}

View File

@@ -1,39 +1,58 @@
const express = require('express');
const path = require('path');
const fs = require('fs');
const sqlite3 = require('sqlite3').verbose();
const sharp = require('sharp');
require('dotenv').config();
const app = express();
const PORT = process.env.PORT || 3000;
const API_KEY = process.env.HOTEL777KEY;
if (!API_KEY) {
console.error('FATAL: HOTEL777KEY environment variable not set');
process.exit(1);
}
// Функция для автоматической конвертации изображений
// Middleware
app.use(express.json());
app.use(express.static(path.join(__dirname, 'public')));
// Ensure data directory and database
const dataDir = path.join(__dirname, 'data');
if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir);
const dbPath = path.join(dataDir, 'bookings.db');
const db = new sqlite3.Database(dbPath);
db.run(`CREATE TABLE IF NOT EXISTS bookings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
phone TEXT NOT NULL,
adults INTEGER NOT NULL,
children INTEGER NOT NULL,
checkin_date TEXT NOT NULL,
checkout_date TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`);
// Image conversion (automatically convert JPEG/PNG to WebP)
async function convertImages() {
const imgDir = path.join(__dirname, 'public', 'img');
// Проверяем, существует ли папка img
if (!fs.existsSync(imgDir)) {
console.log('Папка img не найдена, пропускаем конвертацию.');
return;
}
const files = fs.readdirSync(imgDir);
for (const file of files) {
const filePath = path.join(imgDir, file);
const ext = path.extname(file).toLowerCase();
// Проверяем, является ли файл JPG или PNG
if (ext === '.jpg' || ext === '.jpeg' || ext === '.png') {
const fileNameNoExt = path.parse(file).name;
const webpPath = path.join(imgDir, `${fileNameNoExt}.webp`);
// Проверяем, существует ли уже файл .webp для этого изображения
const name = path.parse(file).name;
const webpPath = path.join(imgDir, `${name}.webp`);
if (!fs.existsSync(webpPath)) {
try {
await sharp(filePath)
.webp({ quality: 85 }) // Качество 85 — золотая середина
await sharp(path.join(imgDir, file))
.webp({ quality: 85 })
.toFile(webpPath);
console.log(`✅ Сконвертировано: ${file} -> ${fileNameNoExt}.webp`);
console.log(`✅ Сконвертировано: ${file} -> ${name}.webp`);
} catch (err) {
console.error(`❌ Ошибка при конвертации ${file}:`, err);
}
@@ -42,16 +61,48 @@ async function convertImages() {
}
}
// Раздача статических файлов из папки public
app.use(express.static(path.join(__dirname, 'public')));
// API: POST /api/bookings сохранить новую заявку
app.post('/api/bookings', (req, res) => {
const { name, phone, adults, children, checkin, checkout } = 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) {
if (err) {
console.error(err);
return res.status(500).json({ error: 'Database error' });
}
res.status(201).json({ id: this.lastID, message: 'Booking saved' });
});
stmt.finalize();
});
// API: GET /api/bookings получить список всех заявок (требуется API-ключ)
app.get('/api/bookings', (req, res) => {
const providedKey = req.headers['x-api-key'];
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
FROM bookings ORDER BY created_at DESC`, (err, rows) => {
if (err) {
console.error(err);
return res.status(500).json({ error: 'Database error' });
}
res.json(rows);
});
});
// Serve frontend
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'index.html'));
});
// Сначала конвертируем, потом запускаем сервер
// Start server after image conversion
convertImages().then(() => {
app.listen(PORT, () => {
console.log(`Сервер Hotel 777 запущен: http://localhost:${PORT}`);
console.log(` Hotel 777 server running on http://localhost:${PORT}`);
});
});