This commit is contained in:
2026-05-05 10:59:02 +05:00
parent ea3076a6f1
commit 6786ea0c3e
10 changed files with 236 additions and 68 deletions

6
.env.sample Normal file
View File

@@ -0,0 +1,6 @@
# Пример переменных окружения
# HOTEL777KEY - секретный API-ключ для доступа к /api/bookings
HOTEL777KEY=your-secret-api-key-here
# PORT можно переопределить при необходимости
# PORT=3000

29
README.md Normal file
View File

@@ -0,0 +1,29 @@
Hotel 777 - локальная веб-площадка на Node.js/Express
Стэк:
- Backend: Node.js + Express + sqlite3
- База данных: SQLite (data/bookings.db создаётся локально во время работы)
- Frontend: статические файлы в папке public (index.html, CSS, JS)
- Доп. пакет: sharp для конвертации изображений
Как запустить
- Установить переменную окружения HOTEL777KEY (API ключ). Можно добавить файл .env с примером: HOTEL777KEY=ваш-ключ
- Установить зависимости: npm install
- Запуск в продакшн-режиме: npm run start
- Для разработки: npm run dev (требуется nodemon, установлен как глобальная/локальная зависимость)
- Браузер: перейти к http://localhost:3000
API
- POST /api/bookings: сохраняет новую заявку бронирования (требуется заголовок x-api-key, равный значению HOTEL777KEY)
- GET /api/bookings: получить список заявок (требуется API-ключ в заголовке x-api-key)
- Формы отправки и frontend-логику можно найти в public/scripts.js и соответствующих модульках food.js, location.js, about.js
Файлы проекта
- package.json: зависимости и скрипты запуска
- server.js: основной Express-сервер
- public/: фронтенд-ресурсы (index.html, scripts.js, style.css, food.js, summer-cafe.js и т.д.)
- data/: база данных SQLite (создаётся при запуске)
Примечания
- Файл .gitignore содержит data, .env и node_modules
- В репозитории нет ключа API; создавайте файл .env.example и храните секреты отдельно

View File

@@ -4,5 +4,9 @@
"express": "^5.2.1",
"sharp": "^0.34.5",
"sqlite3": "^6.0.1"
},
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js"
}
}

73
public/food.js Normal file
View File

@@ -0,0 +1,73 @@
// 食 block: Кухня загружается отдельно как блок на странице
function generateFoodHTML(lang) {
const t = (k) => (window.translations[lang] && window.translations[lang][k]) || k;
return `
<section>
<h2 class="section-title animate" data-i18n="food_title">${t('food_title')}</h2>
<div class="about-grid">
<div class="about-image-wrapper animate delay-1">
<img src="img/food.webp" alt="${t('food_subtitle')}" class="about-img" loading="lazy">
</div>
<div class="about-text animate delay-2">
<h3>${t('food_subtitle')}</h3>
<p>${t('food_text')}</p>
<div class="facts-grid food-features">
<div class="fact-card">
<div class="fact-icon">🍽️</div>
<div class="fact-text">${t('food_request')}</div>
</div>
<div class="fact-card">
<div class="fact-icon">👨‍🍳</div>
<div class="fact-text">${t('food_chef')}</div>
</div>
<div class="fact-card">
<div class="fact-icon">🕒</div>
<div class="fact-text">${t('food_breakfast')}</div>
</div>
<div class="fact-card">
<div class="fact-icon">🕒</div>
<div class="fact-text">${t('food_lunch')}</div>
</div>
<div class="fact-card">
<div class="fact-icon">🕒</div>
<div class="fact-text">${t('food_dinner')}</div>
</div>
</div>
<p class="food-note">📢 <em>${t('food_note')}</em></p>
</div>
</div>
</section>
`;
}
function renderFood(lang) {
const foodSection = document.getElementById('food');
if (!foodSection) return;
foodSection.innerHTML = generateFoodHTML(lang);
// Анимации на повторный вход
document.querySelectorAll('.animate').forEach(el => {
if (el.style.animationPlayState !== 'running') {
el.style.animationPlayState = 'paused';
}
});
}
function updateFoodLanguage(lang) {
if (document.getElementById('food').innerHTML.trim() !== '') {
// обновляем тексты, если уже отрисован блок
const header = document.querySelector('#food .section-title');
if (header) header.innerHTML = window.translations[lang].food_title;
// Перерисуем контент для корректной подстановки
renderFood(lang);
} else {
renderFood(lang);
}
}
window.updateFoodLanguage = updateFoodLanguage;
document.addEventListener('DOMContentLoaded', () => {
const currentLang = localStorage.getItem('siteLang') || 'ru';
renderFood(currentLang);
});

BIN
public/img/summer-cafe.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 904 KiB

BIN
public/img/summer-cafe.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

View File

@@ -3,7 +3,8 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Hotel 777 — отдых в Абхазии, Мгудзырхуа. Комфортные номера, пляж, домашняя кухня.">
<meta name="description" content="Hotel 777 — отдых в Абхазии, Мгудзырхуа. Отель у моря, пляж, домашняя кухня, летнее кафе, Gudauta и Золотой берег.">
<meta name="keywords" content="отдых в абхазии, отель, гудаута, золотой берег, абхазия отдых, пляж, кухня абхазская, жилье абхазия, Gudauta, Abkhazia, Hotel 777">
<title>Hotel 777 | Отдых в Абхазии</title>
<link rel="stylesheet" href="style.css">
<link rel="icon" href="img/favicon.ico" type="image/x-icon">
@@ -22,10 +23,10 @@
<a href="#location" data-i18n="nav_location">Где мы</a>
<a href="#booking" data-i18n="nav_booking">Бронь</a>
</nav>
<select id="langSwitch" class="lang-switch" aria-label="Выбор языка">
<option value="ru">Русский</option>
<option value="ab">Аҧсуа</option>
<select id="langSwitch" class="lang-switch" aria-label="Выбор языка">
<option value="ru" selected>Русский</option>
<option value="en">English</option>
<option value="ab">Аҧсуа</option>
</select>
</header>
@@ -39,43 +40,11 @@
<!-- Секция "О нас" (динамически через about.js) -->
<section id="about" class="white-bg"></section>
<!-- Секция "Кухня" (с карточками питания) -->
<section id="food">
<h2 class="section-title animate" data-i18n="food_title">Вкус Абхазии</h2>
<div class="about-grid">
<div class="about-image-wrapper animate delay-1">
<img src="img/food.webp" alt="Абхазская кухня" class="about-img" loading="lazy">
</div>
<div class="about-text animate delay-2">
<h3 data-i18n="food_subtitle">Домашняя кухня из местных продуктов</h3>
<p data-i18n="food_text">Почувствуйте настоящее гостеприимство! Мы готовим из того, что выросло прямо здесь: свежайший сыр сулугуни, ароматная абыста, сочные овощи с грядки и домашнее вино.</p>
<!-- Кухня будет загружаться отдельным скриптом food.js -->
<section id="food"></section>
<div class="facts-grid food-features">
<div class="fact-card">
<div class="fact-icon">🍽️</div>
<div class="fact-text" data-i18n="food_request">Учитываем пожелания по питанию</div>
</div>
<div class="fact-card">
<div class="fact-icon">👨‍🍳</div>
<div class="fact-text" data-i18n="food_chef">Приготовим блюдо для Вас спросите у шефа</div>
</div>
<div class="fact-card">
<div class="fact-icon">🕒</div>
<div class="fact-text" data-i18n="food_breakfast">Завтрак: с 8:30 до 10:00</div>
</div>
<div class="fact-card">
<div class="fact-icon">🕒</div>
<div class="fact-text" data-i18n="food_lunch">Обед: с 12:00 до 14:00</div>
</div>
<div class="fact-card">
<div class="fact-icon">🕒</div>
<div class="fact-text" data-i18n="food_dinner">Ужин: с 19:00 до 21:00</div>
</div>
</div>
<p class="food-note" data-i18n="food_note">📢 <em>Сообщите администратору о любых предпочтениях при заселении мы всё организуем!</em></p>
</div>
</div>
</section>
<!-- Летнее кафе будет добавлено в будущем (фото можно вставить позже) -->
<section id="summer-cafe" class="white-bg"></section>
<!-- Секция "Где мы" (без карты, только указатель и города) -->
<section id="location" class="white-bg"></section>
@@ -139,5 +108,7 @@
<script src="about.js"></script>
<script src="location.js"></script>
<script src="hero-slideshow.js"></script>
<script src="food.js"></script>
<script src="summer-cafe.js"></script>
</body>
</html>

View File

@@ -10,7 +10,7 @@ window.translations = {
hero_btn: "Забронировать номер",
about_title: "Море в шаговой доступности",
about_subtitle: "Бескрайние пляжи Гудауты",
about_text: "Наш отель расположен в живописном селе Мгудзырхуа. Мы предлагаем комфортные номера и прямой выход к широкому, чистому галечно-песчаному пляжу.",
about_text: "Наш отель расположен в живописном селе Мгудзырхуа на берегу Черного моря. Мы предлагаем комфортные номера с современными удобствами, прямой выход к широкому галечно-песчаному пляжу и впечатляющие виды на Кавказские горы. В окрестностях можно прогуляться по историческим улочкам, попробовать свежие местные продукты и блюда абхазской кухни. Гости ценят спокойствие, уют и близость к природе; для семей предусмотрены удобства и развлечения на территории.",
about_extra: "Гудаутский район известен как «Золотой берег Абхазии» здесь самые широкие пляжи, прогретое море и уникальный микроклимат, сочетающий горный и морской воздух.",
fact1: "🏝️ Золотой берег Абхазии",
fact2: "🌡️ Температура моря до +28°C летом",
@@ -89,7 +89,12 @@ window.translations = {
"🏖️ Широкие галечные пляжи — одни из лучших в Абхазии.",
"🍇 Традиционные виноградники и знаменитые сорта винограда."
],
loc_facts_default: "✨ Удивительные места ждут вас!"
loc_facts_default: "✨ Удивительные места ждут вас!",
summer_title: "Летнее кафе у моря",
summer_subtitle: "Свежие блюда и летний бриз",
summer_text: "Летнее кафе приглашает вас насладиться лёгкими блюдами на открытом воздухе. Экзотические фрукты, прохладительные напитки и тёплый морской воздух создают идеальный день у моря.",
summer_note: "Мы рекомендуем забронировать столик на закате; фотографии будут добавлены позже.",
summer_alt: "Летнее кафе у моря"
},
en: {
nav_about: "About Us",
@@ -101,7 +106,7 @@ window.translations = {
hero_btn: "Book a room",
about_title: "Sea within walking distance",
about_subtitle: "Endless beaches of Gudauta",
about_text: "Our hotel is located in the picturesque village of Mgudzyrkhua. We offer comfortable rooms and direct access to the wide, clean pebble-sand beach.",
about_text: "Our hotel is located in the picturesque village of Mgudzyrkhua on the shores of the Black Sea. We offer comfortable rooms with modern amenities, direct access to a wide pebble-sand beach, and stunning views of the Caucasus mountains. Nearby you can stroll historic streets, enjoy fresh local produce, and sample authentic Abkhaz cuisine. We welcome guests for short visits and extended stays with family-friendly services.",
about_extra: "The Gudauta district is known as the 'Golden Beach of Abkhazia' the widest beaches, warm sea, and a unique microclimate combining mountain and sea air.",
fact1: "🏝️ Golden Beach of Abkhazia",
fact2: "🌡️ Sea temperature up to +28°C in summer",
@@ -180,7 +185,12 @@ window.translations = {
"🏖️ Wide pebble beaches some of the best in Abkhazia.",
"🍇 Traditional vineyards and famous grape varieties."
],
loc_facts_default: "✨ Amazing places are waiting for you!"
loc_facts_default: "✨ Amazing places are waiting for you!",
summer_title: "Summer Café by the Sea",
summer_subtitle: "Fresh dishes and summer breeze",
summer_text: "The Summer Café invites you to enjoy light meals outdoors. Exotic fruits, cold drinks and the sea breeze create a perfect seaside day.",
summer_note: "We recommend reserving a table at sunset; photos will be added later.",
summer_alt: "Summer Café by the Sea"
},
ab: {
nav_about: "Ҳара ҳхәыҷра",
@@ -301,7 +311,36 @@ document.addEventListener("DOMContentLoaded", () => {
// Локализация основных элементов (с data-i18n)
const langSelect = document.getElementById('langSwitch');
let currentLang = localStorage.getItem('siteLang') || 'ru';
// По умолчанию выбор — русский, затем английский, абхазский
let currentLang = localStorage.getItem('siteLang') || 'ru';
// Применить язык без перезагрузки страницы
function setLanguage(lang) {
if (!lang || !window.translations[lang]) return;
const previousLang = localStorage.getItem('siteLang');
localStorage.setItem('siteLang', lang);
// Обновление текстов на странице
document.querySelectorAll('[data-i18n]').forEach(el => {
const key = el.getAttribute('data-i18n');
if (window.translations[lang][key] !== undefined) el.innerHTML = window.translations[lang][key];
});
document.querySelectorAll('[data-i18n-ph]').forEach(el => {
const key = el.getAttribute('data-i18n-ph');
if (window.translations[lang][key] !== undefined) el.placeholder = window.translations[lang][key];
});
// Обновление модульных секций
if (typeof window.updateLocationLanguage === 'function') window.updateLocationLanguage(lang);
if (typeof window.updateAboutLanguage === 'function') window.updateAboutLanguage(lang);
if (typeof window.updateFoodLanguage === 'function') window.updateFoodLanguage(lang);
if (typeof window.updateSummerCafeLanguage === 'function') window.updateSummerCafeLanguage(lang);
// Принудительное полное обновление страницы при смене языка
if (previousLang && previousLang !== lang) {
window.location.reload();
}
}
const updateText = (lang) => {
document.querySelectorAll('[data-i18n]').forEach(el => {
@@ -320,11 +359,19 @@ document.addEventListener("DOMContentLoaded", () => {
if (typeof window.updateAboutLanguage === 'function') {
window.updateAboutLanguage(lang);
}
if (typeof window.updateFoodLanguage === 'function') {
window.updateFoodLanguage(lang);
}
if (typeof window.updateSummerCafeLanguage === 'function') {
window.updateSummerCafeLanguage(lang);
}
};
langSelect.value = currentLang;
updateText(currentLang);
langSelect.onchange = (e) => updateText(e.target.value);
// Применяем язык на старте
setLanguage(currentLang);
// Обработчик смены языка без перезагрузки страницы
langSelect.onchange = (e) => setLanguage(e.target.value);
// Cookie
if (!localStorage.getItem('cookiesAccepted')) {

View File

@@ -28,8 +28,8 @@ body {
}
header {
background: rgba(0, 18, 25, 0.95);
color: var(--white);
background: #ffffff;
color: #1f2937;
padding: 1rem 5%;
position: fixed;
width: 90%;
@@ -37,8 +37,9 @@ header {
display: flex;
justify-content: space-between;
align-items: center;
backdrop-filter: blur(10px);
box-shadow: 0 2px 15px rgba(0, 0, 0, 0.2);
backdrop-filter: blur(6px);
border-bottom: 1px solid #e5e7eb;
box-shadow: 0 2px 15px rgba(0, 0, 0, 0.05);
}
.logo {
@@ -48,9 +49,9 @@ header {
}
.lang-switch {
background: rgba(255, 255, 255, 0.1);
color: var(--white);
border: 1px solid rgba(255, 255, 255, 0.3);
background: #fff;
color: #1f2937;
border: 1px solid #d1d5db;
padding: 6px 10px;
border-radius: 8px;
cursor: pointer;
@@ -58,12 +59,12 @@ header {
}
.lang-switch option {
background: var(--dark);
color: var(--white);
color: #1f2937;
background: #fff;
}
nav a {
color: var(--white);
color: #1f2937;
text-decoration: none;
margin-left: clamp(10px, 2vw, 20px);
font-weight: 500;

37
public/summer-cafe.js Normal file
View File

@@ -0,0 +1,37 @@
// Летнее кафе: блок на сайте с текстами на разных языках
function generateSummerCafeHTML(lang) {
const t = (k) => (window.translations[lang] && window.translations[lang][k]) || k;
return `
<section id="summer-cafe-section" class="summer-cafe">
<h2 class="section-title animate" data-i18n="summer_title">${t('summer_title')}</h2>
<div class="about-grid">
<div class="about-text animate delay-1">
<h3>${t('summer_subtitle')}</h3>
<p>${t('summer_text')}</p>
<p class="summer-note" style="font-style:italic;">${t('summer_note') || ''}</p>
</div>
<div class="about-image-wrapper animate delay-2">
<!-- Реальное фото летнего кафе -->
<img src="img/summer-cafe.webp" alt="${t('summer_alt') || 'Летнее кафе'}" class="about-img" loading="lazy">
</div>
</div>
</section>
`;
}
function renderSummerCafe(lang) {
const container = document.getElementById('summer-cafe');
if (!container) return;
container.innerHTML = generateSummerCafeHTML(lang);
}
function updateSummerCafeLanguage(lang) {
renderSummerCafe(lang);
}
window.updateSummerCafeLanguage = updateSummerCafeLanguage;
document.addEventListener('DOMContentLoaded', () => {
const currentLang = localStorage.getItem('siteLang') || 'ru';
renderSummerCafe(currentLang);
});