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 };