.
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,2 +1,3 @@
|
||||
node_modules
|
||||
package-lock.json
|
||||
package-lock.json
|
||||
.env
|
||||
BIN
data/bookings.db
Normal file
BIN
data/bookings.db
Normal file
Binary file not shown.
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>`;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
89
server.js
89
server.js
@@ -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}`);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user