Files
kalugin66 1559abdb23
All checks were successful
Deploy hotel / deploy-kdo (push) Successful in 56s
26
2026-09-03 00:57:34 +05:00

490 lines
21 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const path = require('path');
const fs = require('fs');
const translationsModule = require('../translations');
let db;
const LANGS = ['ru', 'en', 'ab'];
const LANG_LABELS = { ru: 'Русский', en: 'English', ab: 'Аԥсуа' };
const DEFAULT_ABOUT_IMAGE = 'img/123f15ff3-55ae-428f-8af9-7d53c01d28a5.webp';
const DEFAULT_GALLERY = [
{ image: 'img/h777.webp', overlay: 'gallery.overlay.hotel' },
{ image: 'img/16536f4b7-0c1c-4bbb-8537-96bbdd2e0886.webp', overlay: 'gallery.overlay.beach' },
{ image: 'img/1e4e22546-0208-4ba8-96fb-02052aecc8b5.webp', overlay: 'gallery.overlay.sunset' },
{ image: 'img/1e1ad70fe-ff01-4f94-9ca9-4926eba5bdd2.webp', overlay: 'gallery.overlay.mountains' },
{ image: 'img/123f15ff3-55ae-428f-8af9-7d53c01d28a5.webp', overlay: 'gallery.overlay.river' }
];
function getImagesDir() {
const dir = path.join(__dirname, '..', '..', 'data', 'content_images');
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
return dir;
}
function t(key, lang) {
const dict = translationsModule.translations[lang];
return (dict && dict[key]) || translationsModule.translations.ru[key] || '';
}
function init(database) {
db = database;
seedDefaults();
}
function seedDefaults() {
db.get('SELECT COUNT(*) AS count FROM gallery_items', [], (err, row) => {
if (err || !row || row.count > 0) return;
const stmt = db.prepare(
'INSERT INTO gallery_items (image_path, overlay_ru, overlay_en, overlay_ab, sort_order, is_active) VALUES (?, ?, ?, ?, ?, 1)'
);
DEFAULT_GALLERY.forEach((g, i) => {
stmt.run(g.image, t(g.overlay, 'ru'), t(g.overlay, 'en'), t(g.overlay, 'ab'), i + 1);
});
stmt.finalize();
console.log('✅ Default gallery initialized');
});
db.get('SELECT path FROM content_images WHERE slug = ?', ['about'], (err, row) => {
if (err || (row && row.path)) return;
db.run('INSERT OR IGNORE INTO content_images (slug, path) VALUES (?, ?)', ['about', DEFAULT_ABOUT_IMAGE]);
});
}
// ---------------------------------------------------------------- helpers
function qall(sql, params = []) {
return new Promise((resolve, reject) => {
db.all(sql, params, (err, rows) => (err ? reject(err) : resolve(rows)));
});
}
function qget(sql, params = []) {
return new Promise((resolve, reject) => {
db.get(sql, params, (err, row) => (err ? reject(err) : resolve(row)));
});
}
function qrun(sql, params = []) {
return new Promise((resolve, reject) => {
db.run(sql, params, function(err) { err ? reject(err) : resolve(this); });
});
}
function mergeTranslations(base, overrides) {
const merged = { ...base };
overrides.forEach(o => { merged[o.key] = o.value; });
return merged;
}
function getMergedTranslations(lang, callback) {
const base = translationsModule.getTranslations(lang) || translationsModule.getTranslations('ru');
db.all('SELECT key, value FROM content_overrides WHERE lang = ?', [lang], (err, overrides) => {
if (err) return callback(err, null);
callback(null, mergeTranslations(base, overrides || []));
});
}
// ---------------------------------------------------------------- manifest
function field(key, label, type) {
return { key, label, type: type || 'text' };
}
const OPERATORS = [
{ slug: 'mts', label: 'МТС' },
{ slug: 'megafon', label: 'МегаФон' },
{ slug: 'beeline', label: 'Билайн' },
{ slug: 'yota', label: 'Yota' },
{ slug: 'tele2', label: 'Tele2' }
];
const OPERATOR_FIELDS = [
['name', 'Название', 'text'],
['plan', 'Тариф', 'text'],
['calls_in', 'Входящие', 'text'],
['calls_out', 'Исходящие в РФ', 'text'],
['internet', 'Интернет', 'text'],
['bonus', 'Бонусы', 'text'],
['activation', 'Как подключить', 'textarea']
];
const BLOCKS = [
{
slug: 'hero', title: 'Главный экран',
fields: [
field('hero.badge', 'Бейдж'),
field('hero.title', 'Заголовок (после Hotel 777)'),
field('hero.subtitle', 'Подзаголовок', 'textarea'),
field('hero.btn_rooms', 'Кнопка «Выбрать номер»'),
field('hero.btn_call', 'Кнопка «Позвонить»'),
field('hero.btn_whatsapp', 'Кнопка WhatsApp'),
field('hero.btn_telegram', 'Кнопка Telegram'),
field('hero.btn_max', 'Кнопка Max'),
field('hero.stat_sea', 'Статистика: море'),
field('hero.stat_categories', 'Статистика: категории'),
field('hero.stat_guests', 'Статистика: гости'),
field('hero.stat_days', 'Статистика: дни')
]
},
{
slug: 'about', title: 'О гостинице', image: true,
fields: [
field('about.badge', 'Бейдж'),
field('about.title', 'Заголовок (перед Hotel 777)'),
field('about.desc1', 'Описание 1', 'textarea'),
field('about.desc2', 'Описание 2', 'textarea'),
field('about.badge_img', 'Бейдж на фото'),
field('about.feature1_title', 'Преимущество 1 — название'),
field('about.feature1_desc', 'Преимущество 1 — описание'),
field('about.feature2_title', 'Преимущество 2 — название'),
field('about.feature2_desc', 'Преимущество 2 — описание'),
field('about.feature3_title', 'Преимущество 3 — название'),
field('about.feature3_desc', 'Преимущество 3 — описание'),
field('about.feature4_title', 'Преимущество 4 — название'),
field('about.feature4_desc', 'Преимущество 4 — описание')
]
},
{
slug: 'rooms', title: 'Номера',
fields: [
field('rooms.badge', 'Бейдж'),
field('rooms.title', 'Заголовок'),
field('rooms.subtitle', 'Подзаголовок', 'textarea')
]
},
{
slug: 'gallery', title: 'Галерея',
fields: [
field('gallery.badge', 'Бейдж'),
field('gallery.title', 'Заголовок'),
field('gallery.subtitle', 'Подзаголовок', 'textarea')
],
galleryManager: true
},
{
slug: 'activities', title: 'Развлечения',
fields: [
field('activities.badge', 'Бейдж'),
field('activities.title', 'Заголовок'),
field('activities.subtitle', 'Подзаголовок', 'textarea')
]
},
{
slug: 'travel_tips', title: 'Советы туриста',
fields: [
field('travel_tips.badge', 'Бейдж'),
field('travel_tips.title', 'Заголовок'),
field('travel_tips.subtitle', 'Подзаголовок', 'textarea'),
field('travel_tips.important.title', 'Карточка «Важные моменты» — заголовок'),
...[1, 2, 3, 4].map(i => [
field(`travel_tips.important.${i}.title`, `Важное ${i} — название`),
field(`travel_tips.important.${i}.desc`, `Важное ${i} — описание`, 'textarea')
]).flat(),
field('travel_tips.customs.title', 'Карточка «Национальные особенности» — заголовок'),
...[1, 2, 3, 4].map(i => [
field(`travel_tips.customs.${i}.title`, `Особенность ${i} — название`),
field(`travel_tips.customs.${i}.desc`, `Особенность ${i} — описание`, 'textarea')
]).flat(),
field('travel_tips.mobile.title', 'Мобильная связь — заголовок'),
field('travel_tips.mobile.subtitle', 'Мобильная связь — подзаголовок', 'textarea'),
field('travel_tips.operators.russian.title', 'Российские операторы — заголовок'),
...OPERATORS.map(op => [
field(`travel_tips.operators.${op.slug}.name`, `Оператор ${op.label} — название`),
...OPERATOR_FIELDS.map(([sub, label, type]) =>
field(`travel_tips.operators.${op.slug}.${sub}`, `Оператор ${op.label} — ${label}`, type)
)
]).flat(),
field('travel_tips.operators.mts.badge', 'МТС — бейдж «Лучшее предложение»'),
field('travel_tips.operators.local.title', 'Местные операторы — заголовок'),
field('travel_tips.operators.local.desc', 'Местные операторы — описание', 'textarea'),
field('travel_tips.operators.local.note', 'Местные операторы — примечание', 'textarea'),
field('travel_tips.note', 'Примечание внизу', 'textarea')
]
},
{
slug: 'reviews', title: 'Отзывы',
fields: [
field('reviews.title', 'Заголовок'),
field('reviews.subtitle', 'Подзаголовок', 'textarea'),
field('reviews.write_first', 'Текст «будьте первым»')
]
},
{
slug: 'host', title: 'Хозяин',
fields: [
field('host.badge', 'Бейдж'),
field('host.title', 'Имя хозяина'),
field('host.quote', 'Цитата', 'textarea'),
field('host.desc1', 'Описание 1', 'textarea'),
field('host.desc2', 'Описание 2', 'textarea')
]
},
{
slug: 'contact', title: 'Контакты',
fields: [
field('contact.badge', 'Бейдж'),
field('contact.title', 'Заголовок'),
field('contact.subtitle', 'Подзаголовок', 'textarea'),
field('contact.host_label', 'Подпись «Хозяин гостиницы»'),
field('contact.host_name', 'Имя хозяина'),
field('contact.address_label', 'Подпись «Адрес»'),
field('contact.address_value', 'Значение адреса', 'textarea'),
field('contact.phone_label', 'Подпись «Телефон»'),
field('contact.hours_label', 'Подпись «Режим работы»'),
field('contact.hours_value', 'Значение режима работы'),
field('contact.location_label', 'Подпись «Расположение»'),
field('contact.location_value', 'Значение расположения', 'textarea'),
field('contact.whatsapp', 'Кнопка WhatsApp'),
field('contact.map_area', 'Подпись на карте'),
field('contact.map_desc', 'Описание на карте', 'textarea')
]
},
{
slug: 'cta', title: 'Блок бронирования (CTA)',
fields: [
field('cta.title', 'Заголовок'),
field('cta.subtitle', 'Подзаголовок', 'textarea')
]
},
{
slug: 'footer', title: 'Подвал',
fields: [
field('footer.desc', 'Описание', 'textarea'),
field('footer.nav_title', 'Заголовок «Навигация»'),
field('footer.services_title', 'Заголовок «Услуги»'),
field('footer.contact_title', 'Заголовок «Контакты»'),
field('footer.services_econom', 'Услуга: Номера Эконом'),
field('footer.services_standard', 'Услуга: Номера Стандарт'),
field('footer.services_vip', 'Услуга: Номера VIP'),
field('footer.services_sup', 'Услуга: Аренда SUP-бордов'),
field('footer.services_canteen', 'Услуга: Столовая'),
field('footer.copyright', 'Копирайт', 'textarea')
]
}
];
// ---------------------------------------------------------------- public
function getPublicGallery(req, res) {
const lang = req.query.lang || 'ru';
qall('SELECT id, image_path, overlay_ru, overlay_en, overlay_ab FROM gallery_items WHERE is_active = 1 ORDER BY sort_order ASC, id ASC')
.then(rows => {
const items = rows.map(r => ({
id: r.id,
image: r.image_path,
overlay: r['overlay_' + lang] || r.overlay_ru || ''
}));
res.json(items);
})
.catch(err => {
console.error('Get public gallery error:', err);
res.status(500).json({ error: 'Database error' });
});
}
function getPublicSite(req, res) {
qall('SELECT slug, path FROM content_images')
.then(rows => {
const images = {};
rows.forEach(r => { images[r.slug] = r.path; });
if (!images.about) images.about = DEFAULT_ABOUT_IMAGE;
res.json({ images });
})
.catch(err => {
console.error('Get site content error:', err);
res.status(500).json({ error: 'Database error' });
});
}
// ---------------------------------------------------------------- admin
function getAdminContent(req, res) {
Promise.all([
qall('SELECT lang, key, value FROM content_overrides'),
qall('SELECT slug, path FROM content_images'),
qall('SELECT * FROM gallery_items ORDER BY sort_order ASC, id ASC')
]).then(([overrides, images, gallery]) => {
const values = {};
LANGS.forEach(lang => {
values[lang] = mergeTranslations(
translationsModule.getTranslations(lang) || translationsModule.getTranslations('ru'),
overrides.filter(o => o.lang === lang)
);
});
const imagesMap = {};
images.forEach(r => { imagesMap[r.slug] = r.path; });
if (!imagesMap.about) imagesMap.about = DEFAULT_ABOUT_IMAGE;
res.json({
langs: LANGS,
langLabels: LANG_LABELS,
blocks: BLOCKS,
values,
images: imagesMap,
gallery
});
}).catch(err => {
console.error('Get admin content error:', err);
res.status(500).json({ error: 'Database error' });
});
}
function saveOverrides(req, res) {
const { lang, values } = req.body;
if (!LANGS.includes(lang)) return res.status(400).json({ error: 'Invalid language' });
if (!values || typeof values !== 'object') return res.status(400).json({ error: 'Values required' });
const defaults = translationsModule.getTranslations(lang) || translationsModule.getTranslations('ru');
const entries = Object.entries(values);
let processed = 0;
const fail = (err) => {
console.error('Save overrides error:', err);
res.status(500).json({ error: 'Database error' });
};
if (entries.length === 0) return res.json({ message: 'Saved' });
entries.forEach(([key, value]) => {
const val = String(value == null ? '' : value);
const isDefault = defaults[key] !== undefined && defaults[key] === val;
const sql = isDefault
? 'DELETE FROM content_overrides WHERE lang = ? AND key = ?'
: 'INSERT INTO content_overrides (lang, key, value, updated_at) VALUES (?, ?, ?, CURRENT_TIMESTAMP) ON CONFLICT(lang, key) DO UPDATE SET value = excluded.value, updated_at = CURRENT_TIMESTAMP';
const params = isDefault ? [lang, key] : [lang, key, val];
db.run(sql, params, (err) => {
if (err) return fail(err);
processed++;
if (processed === entries.length) res.json({ message: 'Saved', saved: entries.length });
});
});
}
function createGalleryItem(req, res) {
const { image_path, overlay_ru, overlay_en, overlay_ab, sort_order } = req.body;
if (!image_path) return res.status(400).json({ error: 'Image required' });
db.get(
'SELECT COALESCE(MAX(sort_order), 0) AS max FROM gallery_items',
[],
(err, row) => {
if (err) return res.status(500).json({ error: 'Database error' });
const order = (sort_order !== undefined && sort_order !== null && sort_order !== '') ? parseInt(sort_order, 10) : (row.max + 1);
db.run(
'INSERT INTO gallery_items (image_path, overlay_ru, overlay_en, overlay_ab, sort_order, is_active) VALUES (?, ?, ?, ?, ?, 1)',
[image_path, overlay_ru || '', overlay_en || '', overlay_ab || '', order],
function(err2) {
if (err2) {
console.error('Create gallery item error:', err2);
return res.status(500).json({ error: 'Database error' });
}
res.status(201).json({ message: 'Gallery item created', id: this.lastID });
}
);
}
);
}
function updateGalleryItem(req, res) {
const id = parseInt(req.params.id, 10);
const allowed = ['image_path', 'overlay_ru', 'overlay_en', 'overlay_ab', 'sort_order', 'is_active'];
const fields = [];
const values = [];
allowed.forEach(f => {
if (req.body[f] !== undefined && req.body[f] !== null) {
fields.push(f + ' = ?');
values.push(req.body[f]);
}
});
if (!fields.length) return res.status(400).json({ error: 'No fields to update' });
values.push(id);
db.run(`UPDATE gallery_items SET ${fields.join(', ')} WHERE id = ?`, values, function(err) {
if (err) {
console.error('Update gallery item error:', err);
return res.status(500).json({ error: 'Database error' });
}
if (this.changes === 0) return res.status(404).json({ error: 'Gallery item not found' });
res.json({ message: 'Gallery item updated', id });
});
}
function deleteGalleryItem(req, res) {
const id = parseInt(req.params.id, 10);
db.get('SELECT image_path FROM gallery_items WHERE id = ?', [id], (err, row) => {
if (err) {
console.error('Find gallery item error:', err);
return res.status(500).json({ error: 'Database error' });
}
if (!row) return res.status(404).json({ error: 'Gallery item not found' });
db.run('DELETE FROM gallery_items WHERE id = ?', [id], function(err2) {
if (err2) {
console.error('Delete gallery item error:', err2);
return res.status(500).json({ error: 'Database error' });
}
const filePath = row.image_path;
if (filePath && !filePath.startsWith('http') && !filePath.startsWith('img/')) {
const fullPath = path.join(__dirname, '..', '..', filePath);
if (fs.existsSync(fullPath)) {
fs.unlink(fullPath, (unlinkErr) => {
if (unlinkErr) console.error('Failed to delete file:', unlinkErr);
});
}
}
res.json({ message: 'Gallery item deleted', id });
});
});
}
function uploadImageHandler(req, res) {
if (!req.file) return res.status(400).json({ error: 'No file uploaded' });
const filename = req.file.filename;
const filePath = 'data/content_images/' + filename;
res.json({ message: 'Image uploaded', path: filePath, filename });
}
function saveBlockImage(req, res) {
const { slug, path: imagePath } = req.body;
if (!slug || !imagePath) return res.status(400).json({ error: 'Slug and path required' });
db.run(
'INSERT INTO content_images (slug, path, updated_at) VALUES (?, ?, CURRENT_TIMESTAMP) ON CONFLICT(slug) DO UPDATE SET path = excluded.path, updated_at = CURRENT_TIMESTAMP',
[slug, imagePath],
(err) => {
if (err) {
console.error('Save block image error:', err);
return res.status(500).json({ error: 'Database error' });
}
res.json({ message: 'Image saved', slug, path: imagePath });
}
);
}
function setupRoutes(app, authenticateToken, requireAdmin, upload) {
app.get('/api/content/gallery', getPublicGallery);
app.get('/api/content/site', getPublicSite);
app.get('/api/admin/content', authenticateToken, requireAdmin, getAdminContent);
app.put('/api/admin/content/overrides', authenticateToken, requireAdmin, saveOverrides);
app.post('/api/admin/content/gallery', authenticateToken, requireAdmin, createGalleryItem);
app.put('/api/admin/content/gallery/:id', authenticateToken, requireAdmin, updateGalleryItem);
app.delete('/api/admin/content/gallery/:id', authenticateToken, requireAdmin, deleteGalleryItem);
app.post('/api/admin/content/upload', authenticateToken, requireAdmin, upload.single('image'), uploadImageHandler);
app.put('/api/admin/content/images', authenticateToken, requireAdmin, saveBlockImage);
}
module.exports = {
init,
setupRoutes,
getImagesDir,
getMergedTranslations,
getPublicGallery,
getPublicSite,
getAdminContent
};