Compare commits
8 Commits
33025fc74a
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 5fa29fd33d | |||
| 00cf2ebd9a | |||
| aad23d3a96 | |||
| 0b61d5be82 | |||
| c4662c2a63 | |||
| 4dab8eb93b | |||
| af02d216c6 | |||
| 67d4d22358 |
@@ -80,7 +80,6 @@ function initDatabase() {
|
||||
image_path TEXT,
|
||||
qty TEXT,
|
||||
specs TEXT DEFAULT '[]',
|
||||
age_badge TEXT DEFAULT '',
|
||||
sort_order INTEGER DEFAULT 0,
|
||||
is_active INTEGER DEFAULT 1
|
||||
)`);
|
||||
@@ -144,7 +143,11 @@ function initDatabase() {
|
||||
)`);
|
||||
|
||||
db.get(`SELECT COUNT(*) as cnt FROM settings`, (err, row) => {
|
||||
if (row && row.cnt > 0) { resolve(); return; }
|
||||
if (row && row.cnt > 0) {
|
||||
db.run(`INSERT OR IGNORE INTO settings (key, value) VALUES ('logo_path', './img/logo.webp')`);
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
seedAll().then(resolve).catch(reject);
|
||||
});
|
||||
});
|
||||
@@ -177,15 +180,14 @@ async function seedAll() {
|
||||
]);
|
||||
|
||||
await runEach('INSERT OR IGNORE INTO about_settings (key, value) VALUES (?, ?)', [
|
||||
['section_subtitle', 'Компания «СТС-Авто» — надёжный партнёр в сфере демонтажа, оказывает услуги по демонтажу магистральных трубопроводов, объектов инфраструктуры, зданий, сооружений и разделки жд транспорта, с последующей переработкой и утилизацией для предприятий. Мы работаем на всей территории Российской Федерации.'],
|
||||
['section_subtitle', 'Компания «СТС-Авто» — надёжный партнёр в сфере демонтажа, оказывает услуги по демонтажу всех типов магистральных и промысловых сетей (нефтепроводы, газопроводы, водоводы). Мы работаем на всей территории Российской Федерации.'],
|
||||
['main_heading', 'Собственная техника и полный контроль'], ['about_image', './img/hero-bg.webp'],
|
||||
['paragraph_1', 'Наш автопарк состоит из более чем 80 единиц современной спецтехники, возраст которой не превышает 5 лет. Мы используем экскаваторы, краны, погрузчики, самосвалы и тралы ведущих мировых производителей: KAMAZ, Komatsu, Zoomlion, SDLG, Соболь, УАЗ.'],
|
||||
['paragraph_1', 'Наш автопарк состоит из более чем 80 единиц современной спецтехники. Мы используем экскаваторы, краны, погрузчики, самосвалы и тралы ведущих мировых производителей: KAMAZ, Komatsu, Zoomlion, SDLG, Соболь, УАЗ.'],
|
||||
['paragraph_2', 'Все работы выполняются исключительно силами компании — без привлечения субподрядчиков. Это гарантирует полное соответствие качества, сроков и ответственности на каждом этапе проекта.'],
|
||||
['paragraph_3', 'Мы работаем в самых сложных условиях: труднодоступные районы, заболоченные территории, Крайний Север. Гибкие варианты оплаты: работаем по договору, включая вариант расчёта трубой за услуги (бартер с металлоломом после демонтажа).']
|
||||
['paragraph_3', 'Мы работаем в самых сложных условиях: труднодоступные районы, заболоченные территории, Крайний Север. Гибкие варианты оплаты: работаем по договору']
|
||||
]);
|
||||
|
||||
await runEach('INSERT INTO about_features (svg_icon, title, description, sort_order) VALUES (?, ?, ?, ?)', [
|
||||
['<path d="M18.92 6.01C18.72 5.42 18.16 5 17.5 5h-11c-.66 0-1.21.42-1.42 1.01L3 12v8c0 .55.45 1 1 1h1c.55 0 1-.45 1-1v-1h12v1c0 .55.45 1 1 1h1c.55 0 1-.45 1-1v-8l-2.08-5.99zM6.5 16c-.83 0-1.5-.67-1.5-1.5S5.67 13 6.5 13s1.5.67 1.5 1.5S7.33 16 6.5 16zm11 0c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zM5 11l1.5-4.5h11L19 11H5z"/>', 'Парк техники не старше 5 лет', 'Более 80 единиц современной спецтехники', 0],
|
||||
['<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>', 'Без субподрядчиков', 'Все работы силами компании', 1],
|
||||
['<path d="M19.35 10.04A7.49 7.49 0 0012 4C9.11 4 6.6 5.64 5.35 8.04A5.994 5.994 0 000 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96z"/>', 'Сложные условия', 'Болота, Крайний Север, труднодоступные районы', 2],
|
||||
['<path d="M11.8 10.9c-2.27-.59-3-1.2-3-2.15 0-1.09 1.01-1.85 2.7-1.85 1.78 0 2.44.85 2.5 2.1h2.21c-.07-1.72-1.12-3.3-3.21-3.81V3h-3v2.16c-1.94.42-3.5 1.68-3.5 3.61 0 2.31 1.91 3.46 4.7 4.13 2.5.6 3 1.48 3 2.41 0 .69-.49 1.79-2.7 1.79-2.06 0-2.87-.92-2.98-2.1h-2.2c.12 2.19 1.76 3.42 3.68 3.83V21h3v-2.15c1.95-.37 3.5-1.5 3.5-3.55 0-2.84-2.43-3.81-4.7-4.4z"/>', 'Гибкая оплата', 'Включая расчёт трубой (бартер)', 3]
|
||||
@@ -193,7 +195,6 @@ async function seedAll() {
|
||||
|
||||
const svcIcon = {
|
||||
demolish: '<path d="M19.35 10.04A7.49 7.49 0 0012 4C9.11 4 6.6 5.64 5.35 8.04A5.994 5.994 0 000 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96z"/>',
|
||||
recult: '<path d="M14 6l-3.75 5 2.85 3.8-1.6 1.2C9.81 13.75 7 10 7 10l-6 8h22L14 6z"/>',
|
||||
clear: '<path d="M17 12h-5v5h5v-5zM16 1v2H8V1H6v2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2h-1V1h-2zm3 18H5V8h14v11z"/>',
|
||||
lezh: '<path d="M20 2H4c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-9 14H4v-3h7v3zm0-5H4V8h7v3zm9 5h-7v-3h7v3zm0-5h-7V8h7v3z"/>',
|
||||
scheme1: '<path d="M20 8h-3V4H3c-1.1 0-2 .9-2 2v11h2c0 1.66 1.34 3 3 3s3-1.34 3-3h6c0 1.66 1.34 3 3 3s3-1.34 3-3h2v-5l-3-4zM6 18.5c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm13.5-9l1.96 2.5H17V9.5h2.5zm-1.5 9c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5z"/>',
|
||||
@@ -202,7 +203,6 @@ async function seedAll() {
|
||||
|
||||
await runEach('INSERT INTO services (svg_icon, title, description, items, is_large, sort_order) VALUES (?, ?, ?, ?, ?, ?)', [
|
||||
[svcIcon.demolish, 'Демонтаж нефтегазопроводов, утилизация отходов', 'Выполняем полный цикл работ по демонтажу магистральных и промысловых трубопроводов любого типа и диаметра, а также утилизацию отходов.', '["Нефтепроводы, газопроводы, водоводы — все типы магистральных и промысловых сетей","Полный цикл: от вскрытия и вырезки дефектных участков до вывоза и сдачи металлолома","Работа с трубопроводами любого диаметра в любых климатических условиях","Разделка ж/д транспорта, переработка и утилизация для предприятий"]', 1, 0],
|
||||
[svcIcon.recult, 'Техническая рекультивация земель', 'Восстановление нарушенных земель после проведения строительных и ремонтных работ.', '["Засыпка траншей, планировка, восстановление плодородного слоя","Работа с нефтезагрязнёнными, засоленными землями","Сдача по актам под контролем экологов"]', 0, 1],
|
||||
[svcIcon.clear, 'Расчистка территории от мелколесья и ДКР', 'Подготовка полосы отвода: расчистка от древесно-кустарниковой растительности.', '["Валка, корчёвка, дробление пней","Планировка полосы отвода","Мульчеры и кусторезы на гусеничном ходу"]', 0, 2],
|
||||
[svcIcon.lezh, 'Устройство лежневого настила', 'Обеспечение проходимости для тяжёлой техники по болотам и слабым грунтам.', '["Изготовление секций из бревен и бруса","Укладка перекрытий на слабых грунтах","Обеспечение проходимости для тяжёлой гусеничной техники"]', 0, 3]
|
||||
]);
|
||||
@@ -215,22 +215,22 @@ async function seedAll() {
|
||||
await runEach('INSERT INTO process_steps (step_number, title, description, sort_order) VALUES (?, ?, ?, ?)', [
|
||||
[1, 'Заявка', 'Вы оставляете заявку на сайте, по электронной почте или звоните нам. Наши специалисты оперативно свяжутся с вами для обсуждения деталей проекта, объёма работ и сроков.', 0],
|
||||
[2, 'Выезд специалиста и оценка', 'Наш инженер выезжает на объект для проведения осмотра, оценки объёма работ, определения необходимых ресурсов и техники. По результатам составляется техническое задание и подписывается договор.', 1],
|
||||
[3, 'Выполнение работ', 'Мобилизуем технику и персонал на объект. Выполняем демонтаж, утилизацию отходов и рекультивацию в соответствии с проектной документацией. Ежедневные отчёты о ходе работ.', 2],
|
||||
[3, 'Выполнение работ', 'Мобилизуем технику и персонал на объект. Выполняем демонтаж и утилизацию отходов в соответствии с проектной документацией. Ежедневные отчёты о ходе работ.', 2],
|
||||
[4, 'Сдача объекта', 'По завершении всех работ подписывается акт выполненных работ, передаётся полный комплект закрывающей документации. При необходимости — сопровождение сдачи объекта экспертизе.', 3]
|
||||
]);
|
||||
|
||||
await runEach('INSERT INTO equipment (name, brand, image_path, qty, specs, age_badge, sort_order) VALUES (?, ?, ?, ?, ?, ?, ?)', [
|
||||
['Автокран КАМАЗ КС-55717К-3', 'КАМАЗ', 'ks-45721.webp', '10 единиц', '[{"label":"Грузоподъёмность","value":"25 тонн"},{"label":"Вылет стрелы","value":"до 28 м"},{"label":"Шасси","value":"Полноприводное 6×6"}]', 'Не старше 5 лет', 0],
|
||||
['Автокран Zoomlion ZTC300', 'Zoomlion', 'zoomlion-ztc300.webp', '2 единицы', '[{"label":"Грузоподъёмность","value":"30 тонн"},{"label":"Вылет стрелы","value":"до 32 м"},{"label":"Стрела","value":"Телескопическая"}]', 'Не старше 5 лет', 1],
|
||||
['Экскаватор гусеничный Komatsu PC210-10M0', 'Komatsu', 'komatsu-pc210.webp', '20 единиц', '[{"label":"Масса","value":"21 тонна"},{"label":"Объём ковша","value":"1.0–1.2 м³"},{"label":"Глубина копания","value":"до 6.5 м"}]', 'Не старше 5 лет', 2],
|
||||
['Экскаватор гусеничный Zoomlion ZE215E', 'Zoomlion', 'zoomlion-ze215e.webp', '2 единицы', '[{"label":"Масса","value":"21.5 тонны"},{"label":"Объём ковша","value":"1.1 м³"},{"label":"Аналог","value":"Komatsu PC210"}]', 'Не старше 5 лет', 3],
|
||||
['Погрузчик колёсный SDLG LG933L', 'SDLG', 'sdlg-lg933l.webp', '2 единицы', '[{"label":"Грузоподъёмность","value":"3 тонны"},{"label":"Объём ковша","value":"1.7 м³"},{"label":"Тип","value":"Колёсный"}]', 'Не старше 5 лет', 4],
|
||||
['Соболь 4×4 грузо-пассажирский', 'Соболь', 'sobol-4x4.webp', '13 единиц', '[{"label":"Грузоподъёмность","value":"до 1 т"},{"label":"Привод","value":"Полный 4×4"},{"label":"Назначение","value":"Доставка персонала и запчастей"}]', 'Не старше 5 лет', 5],
|
||||
['Ломовоз с КМУ (кран-манипулятор)', 'Ломовоз', 'lomovoz-kmu.webp', '3 единицы', '[{"label":"Грузоподъёмность КМУ","value":"до 8 т"},{"label":"Объём платформы","value":"до 20 м³"},{"label":"Назначение","value":"Вывоз металлолома"}]', 'Не старше 5 лет', 6],
|
||||
['Седельный тягач Камаз 53504-50', 'КамАЗ', 'kamaz-53504.webp', '6 единиц', '[{"label":"Тяговый класс","value":"до 50 т"},{"label":"Привод","value":"Полный привод"},{"label":"Назначение","value":"Перевозка негабарита и тралов"}]', 'Не старше 5 лет', 7],
|
||||
['Экскаватор-амфибия на понтонной платформе', 'Амфибия', 'amfibia.webp', '4 единицы', '[{"label":"Тип","value":"Гусеничный, плавающий"},{"label":"Работает","value":"На болотах и водных преградах"},{"label":"Особенность","value":"Там, где не видно дна"}]', 'Не старше 5 лет', 8],
|
||||
['УАЗ Пикап (полный привод)', 'УАЗ', 'uaz-pickup.webp', 'Несколько единиц', '[{"label":"Привод","value":"Полный привод"},{"label":"Назначение","value":"Доставка запчастей на месторождения"}]', 'Не старше 5 лет', 9],
|
||||
['Тралы тяжеловозы для доставки спецтехники', 'Тралы', 'tral-1.webp', '3 единицы', '[{"label":"Тяговый класс","value":"до 50 т"},{"label":"Назначение","value":"Доставка спецтехники на объекты"}]', 'Не старше 5 лет', 10]
|
||||
await runEach('INSERT INTO equipment (name, brand, image_path, qty, specs, sort_order) VALUES (?, ?, ?, ?, ?, ?)', [
|
||||
['Автокран КАМАЗ КС-55717К-3', 'КАМАЗ', 'ks-45721.webp', '10 единиц', '[{"label":"Грузоподъёмность","value":"25 тонн"},{"label":"Вылет стрелы","value":"до 28 м"},{"label":"Шасси","value":"Полноприводное 6×6"}]', 0],
|
||||
['Автокран Zoomlion ZTC300', 'Zoomlion', 'zoomlion-ztc300.webp', '2 единицы', '[{"label":"Грузоподъёмность","value":"30 тонн"},{"label":"Вылет стрелы","value":"до 32 м"},{"label":"Стрела","value":"Телескопическая"}]', 1],
|
||||
['Экскаватор гусеничный Komatsu PC210-10M0', 'Komatsu', 'komatsu-pc210.webp', '20 единиц', '[{"label":"Масса","value":"21 тонна"},{"label":"Объём ковша","value":"1.0–1.2 м³"},{"label":"Глубина копания","value":"до 6.5 м"}]', 2],
|
||||
['Экскаватор гусеничный Zoomlion ZE215E', 'Zoomlion', 'zoomlion-ze215e.webp', '2 единицы', '[{"label":"Масса","value":"21.5 тонны"},{"label":"Объём ковша","value":"1.1 м³"},{"label":"Аналог","value":"Komatsu PC210"}]', 3],
|
||||
['Погрузчик колёсный SDLG LG933L', 'SDLG', 'sdlg-lg933l.webp', '2 единицы', '[{"label":"Грузоподъёмность","value":"3 тонны"},{"label":"Объём ковша","value":"1.7 м³"},{"label":"Тип","value":"Колёсный"}]', 4],
|
||||
['Соболь 4×4 грузо-пассажирский', 'Соболь', 'sobol-4x4.webp', '13 единиц', '[{"label":"Грузоподъёмность","value":"до 1 т"},{"label":"Привод","value":"Полный 4×4"},{"label":"Назначение","value":"Доставка персонала и запчастей"}]', 5],
|
||||
['Ломовоз с КМУ (кран-манипулятор)', 'Ломовоз', 'lomovoz-kmu.webp', '3 единицы', '[{"label":"Грузоподъёмность КМУ","value":"до 8 т"},{"label":"Объём платформы","value":"до 20 м³"},{"label":"Назначение","value":"Вывоз металлолома"}]', 6],
|
||||
['Седельный тягач Камаз 53504-50', 'КамАЗ', 'kamaz-53504.webp', '6 единиц', '[{"label":"Тяговый класс","value":"до 50 т"},{"label":"Привод","value":"Полный привод"},{"label":"Назначение","value":"Перевозка негабарита и тралов"}]', 7],
|
||||
['Экскаватор-амфибия на понтонной платформе', 'Амфибия', 'amfibia.webp', '4 единицы', '[{"label":"Тип","value":"Гусеничный, плавающий"},{"label":"Работает","value":"На болотах и водных преградах"},{"label":"Особенность","value":"Там, где не видно дна"}]', 8],
|
||||
['УАЗ Пикап (полный привод)', 'УАЗ', 'uaz-pickup.webp', 'Несколько единиц', '[{"label":"Привод","value":"Полный привод"},{"label":"Назначение","value":"Доставка запчастей на месторождения"}]', 9],
|
||||
['Тралы тяжеловозы для доставки спецтехники', 'Тралы', 'tral-1.webp', '3 единицы', '[{"label":"Тяговый класс","value":"до 50 т"},{"label":"Назначение","value":"Доставка спецтехники на объекты"}]', 10]
|
||||
]);
|
||||
|
||||
await runEach('INSERT INTO geo_regions (name, sort_order) VALUES (?, ?)', [
|
||||
@@ -278,6 +278,7 @@ async function seedAll() {
|
||||
]);
|
||||
|
||||
await runEach('INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)', [
|
||||
['logo_path', './img/logo.webp'],
|
||||
['contact_phone_800', '8 (800) 555-29-31'], ['contact_phone_ekb', '8 (343) 237-25-66'],
|
||||
['contact_email', 'sts_zakaz@mail.ru'], ['contact_address', 'г. Екатеринбург, ул. Московская, 11, оф. 702'],
|
||||
['contact_work_hours', 'Пн–Пт: 9:00 – 18:00|Сб: 10:00 – 15:00'], ['contact_map_image', './img/secret.webp'],
|
||||
@@ -286,7 +287,7 @@ async function seedAll() {
|
||||
['section_services_subtitle', 'Полный спектр работ по демонтажу и утилизации трубопроводной инфраструктуры. Работаем на опасных производственных объектах с собственным парком спецтехники.'],
|
||||
['section_schemes_subtitle', 'Мы разработали гибкие схемы работы, позволяющие выбрать оптимальное решение с учётом ваших целей и бюджета.'],
|
||||
['section_process_subtitle', 'Прозрачный и отлаженный процесс от первого обращения до сдачи объекта. Каждый этап контролируется инженерами компании.'],
|
||||
['section_equipment_subtitle', 'Собственный парк из более чем 80 единиц современной спецтехники ведущих мировых производителей. Возраст всей техники — не старше 5 лет.'],
|
||||
['section_equipment_subtitle', 'Собственный парк из более чем 80 единиц современной спецтехники ведущих мировых производителей.'],
|
||||
['section_licenses_subtitle', 'Все необходимые допуски и сертификаты для выполнения работ на опасных производственных объектах. Полная прозрачность и соответствие требованиям законодательства РФ.'],
|
||||
['section_activity_subtitle', 'Компания «СТС-Авто» активно участвует в социальной и общественной жизни регионов присутствия.'],
|
||||
['section_partners_subtitle', 'Сотрудничаем с ведущими компаниями нефтегазового сектора.'],
|
||||
|
||||
@@ -109,7 +109,7 @@ function setupRoutes(app, auth) {
|
||||
});
|
||||
|
||||
createCrudRoutes(app, auth, 'equipment', {
|
||||
fields: ['name', 'brand', 'image_path', 'qty', 'specs', 'age_badge', 'sort_order', 'is_active']
|
||||
fields: ['name', 'brand', 'image_path', 'qty', 'specs', 'sort_order', 'is_active']
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
const multer = require('multer');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const sharp = require('sharp');
|
||||
|
||||
let db;
|
||||
|
||||
function init(database) { db = database; }
|
||||
@@ -5,6 +10,60 @@ function init(database) { db = database; }
|
||||
function setupRoutes(app, auth) {
|
||||
const { authenticateToken, requireAdmin } = auth;
|
||||
|
||||
const imgDir = path.join(__dirname, '..', '..', 'public', 'img');
|
||||
const favDir = path.join(__dirname, '..', '..', 'public');
|
||||
if (!fs.existsSync(imgDir)) fs.mkdirSync(imgDir, { recursive: true });
|
||||
|
||||
const storage = multer.memoryStorage();
|
||||
const uploadLogo = multer({
|
||||
storage,
|
||||
limits: { fileSize: 5 * 1024 * 1024 },
|
||||
fileFilter: (req, file, cb) => {
|
||||
const allowed = ['.jpg', '.jpeg', '.png', '.webp', '.svg', '.gif', '.bmp'];
|
||||
const ext = path.extname(file.originalname).toLowerCase();
|
||||
if (!allowed.includes(ext)) return cb(new Error('Недопустимый формат файла'));
|
||||
cb(null, true);
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/admin/settings/logo', authenticateToken, requireAdmin, uploadLogo.single('logo'), async (req, res) => {
|
||||
if (!req.file) return res.status(400).json({ error: 'Файл не загружен' });
|
||||
|
||||
try {
|
||||
const ext = path.extname(req.file.originalname).toLowerCase();
|
||||
const logoWebp = path.join(imgDir, 'logo.webp');
|
||||
const fav32 = path.join(favDir, 'favicon-32x32.png');
|
||||
const fav16 = path.join(favDir, 'favicon-16x16.png');
|
||||
const apple = path.join(favDir, 'apple-touch-icon.png');
|
||||
|
||||
const pipeline = sharp(req.file.buffer);
|
||||
|
||||
if (ext !== '.svg') {
|
||||
await pipeline.clone().webp({ quality: 85 }).toFile(logoWebp);
|
||||
} else {
|
||||
fs.writeFileSync(logoWebp, req.file.buffer);
|
||||
}
|
||||
|
||||
if (ext !== '.svg') {
|
||||
await pipeline.clone().resize(32, 32, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(fav32);
|
||||
await pipeline.clone().resize(16, 16, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(fav16);
|
||||
await pipeline.clone().resize(180, 180, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(apple);
|
||||
} else {
|
||||
fs.writeFileSync(fav32, req.file.buffer);
|
||||
fs.writeFileSync(fav16, req.file.buffer);
|
||||
fs.writeFileSync(apple, req.file.buffer);
|
||||
}
|
||||
|
||||
const logoPath = './img/logo.webp';
|
||||
db.run(`INSERT INTO settings (key, value) VALUES ('logo_path', ?) ON CONFLICT(key) DO UPDATE SET value = ?`, [logoPath, logoPath]);
|
||||
|
||||
res.json({ path: logoPath, url: logoPath });
|
||||
} catch (e) {
|
||||
console.error('Ошибка при обработке логотипа:', e);
|
||||
res.status(500).json({ error: 'Ошибка обработки изображения' });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/admin/settings', authenticateToken, requireAdmin, (req, res) => {
|
||||
db.all(`SELECT * FROM settings ORDER BY id`, (err, rows) => {
|
||||
if (err) return res.status(500).json({ error: 'Ошибка БД' });
|
||||
|
||||
@@ -12,63 +12,65 @@ function loadEquipmentGallery(data) {
|
||||
});
|
||||
}
|
||||
|
||||
function loadSiteData() {
|
||||
const data = {};
|
||||
|
||||
function loadTable(table, key) {
|
||||
return new Promise((resolve) => {
|
||||
db.all(`SELECT * FROM ${table} ORDER BY sort_order`, (err, rows) => {
|
||||
if (err) { data[key] = []; resolve(); return; }
|
||||
if (table === 'equipment' || table === 'partners') {
|
||||
rows = rows.filter(r => r.is_active == 1);
|
||||
}
|
||||
if (table === 'services' || table === 'work_schemes') {
|
||||
rows = rows.filter(r => r.is_active == 1);
|
||||
}
|
||||
data[key] = rows;
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function loadSettings(table, key, transformFn) {
|
||||
return new Promise((resolve) => {
|
||||
db.all(`SELECT * FROM ${table} ORDER BY id`, (err, rows) => {
|
||||
if (err) { data[key] = {}; resolve(); return; }
|
||||
if (transformFn) {
|
||||
data[key] = transformFn(rows);
|
||||
} else {
|
||||
const obj = {};
|
||||
rows.forEach(r => { obj[r.key] = r.value; });
|
||||
data[key] = obj;
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.all([
|
||||
loadSettings('hero_settings', 'hero'),
|
||||
loadTable('hero_stats', 'hero_stats'),
|
||||
loadSettings('about_settings', 'about'),
|
||||
loadTable('about_features', 'about_features'),
|
||||
loadTable('services', 'services'),
|
||||
loadTable('work_schemes', 'work_schemes'),
|
||||
loadTable('process_steps', 'process_steps'),
|
||||
loadTable('equipment', 'equipment'),
|
||||
loadEquipmentGallery(data),
|
||||
loadTable('geo_regions', 'geo_regions'),
|
||||
loadSettings('geo_settings', 'geo'),
|
||||
loadTable('licenses', 'licenses'),
|
||||
loadTable('license_gallery', 'license_gallery'),
|
||||
loadTable('public_activities', 'public_activities'),
|
||||
loadTable('activity_gallery', 'activity_gallery'),
|
||||
loadTable('partners', 'partners'),
|
||||
loadSettings('settings', 'settings')
|
||||
]).then(() => data);
|
||||
}
|
||||
|
||||
function setupRoutes(app) {
|
||||
app.get('/api/site-data', (req, res) => {
|
||||
const data = {};
|
||||
|
||||
function loadTable(table, key) {
|
||||
return new Promise((resolve) => {
|
||||
db.all(`SELECT * FROM ${table} ORDER BY sort_order`, (err, rows) => {
|
||||
if (err) { data[key] = []; resolve(); return; }
|
||||
if (table === 'equipment' || table === 'partners') {
|
||||
rows = rows.filter(r => r.is_active == 1);
|
||||
}
|
||||
if (table === 'services' || table === 'work_schemes') {
|
||||
rows = rows.filter(r => r.is_active == 1);
|
||||
}
|
||||
data[key] = rows;
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function loadSettings(table, key, transformFn) {
|
||||
return new Promise((resolve) => {
|
||||
db.all(`SELECT * FROM ${table} ORDER BY id`, (err, rows) => {
|
||||
if (err) { data[key] = {}; resolve(); return; }
|
||||
if (transformFn) {
|
||||
data[key] = transformFn(rows);
|
||||
} else {
|
||||
const obj = {};
|
||||
rows.forEach(r => { obj[r.key] = r.value; });
|
||||
data[key] = obj;
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Promise.all([
|
||||
loadSettings('hero_settings', 'hero'),
|
||||
loadTable('hero_stats', 'hero_stats'),
|
||||
loadSettings('about_settings', 'about'),
|
||||
loadTable('about_features', 'about_features'),
|
||||
loadTable('services', 'services'),
|
||||
loadTable('work_schemes', 'work_schemes'),
|
||||
loadTable('process_steps', 'process_steps'),
|
||||
loadTable('equipment', 'equipment'),
|
||||
loadEquipmentGallery(data),
|
||||
loadTable('geo_regions', 'geo_regions'),
|
||||
loadSettings('geo_settings', 'geo'),
|
||||
loadTable('licenses', 'licenses'),
|
||||
loadTable('license_gallery', 'license_gallery'),
|
||||
loadTable('public_activities', 'public_activities'),
|
||||
loadTable('activity_gallery', 'activity_gallery'),
|
||||
loadTable('partners', 'partners'),
|
||||
loadSettings('settings', 'settings')
|
||||
]).then(() => {
|
||||
res.json(data);
|
||||
});
|
||||
loadSiteData().then(data => res.json(data));
|
||||
});
|
||||
|
||||
app.post('/api/leads', (req, res) => {
|
||||
@@ -83,4 +85,4 @@ function setupRoutes(app) {
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { init, setupRoutes };
|
||||
module.exports = { init, setupRoutes, loadSiteData };
|
||||
|
||||
250
modules/ssrRenderer.js
Normal file
@@ -0,0 +1,250 @@
|
||||
const yearsSince2011 = new Date().getFullYear() - 2011;
|
||||
|
||||
function esc(s) { return String(s||'').replace(/&/g,'&').replace(/"/g,'"').replace(/</g,'<').replace(/>/g,'>'); }
|
||||
|
||||
function imgSrc(path) {
|
||||
if (!path) return './img/hero-bg.webp';
|
||||
if (path.startsWith('./')) return path;
|
||||
if (path.startsWith('http')) return path;
|
||||
if (/^(equipment|partner|license|activity|hero)_images\//.test(path)) return '/data/' + path;
|
||||
return './img/' + path;
|
||||
}
|
||||
|
||||
function renderHero(data) {
|
||||
var h = data.hero || {};
|
||||
var stats = data.hero_stats || [];
|
||||
var phone800 = h.phone_800 || '8 (800) 555-29-31';
|
||||
var phoneEkb = h.phone_ekb || '8 (343) 237-25-66';
|
||||
var email = h.email || 'sts_zakaz@mail.ru';
|
||||
var address = h.address || 'г. Екатеринбург, ул. Московская, 11, оф. 702';
|
||||
|
||||
var contactsHtml = '';
|
||||
if (phone800 && phoneEkb) {
|
||||
contactsHtml += '<div class="hero-contact-item"><svg viewBox="0 0 24 24"><path d="M6.62 10.79a15.053 15.053 0 006.59 6.59l2.2-2.2a1 1 0 011.01-.24c1.12.37 2.33.57 3.58.57a1 1 0 011 1V20a1 1 0 01-1 1A17 17 0 013 4a1 1 0 011-1h3.5a1 1 0 011 1c0 1.25.2 2.46.57 3.58a1 1 0 01-.24 1.01l-2.2 2.2z"/></svg><div><a href="tel:' + phone800.replace(/\D/g,'') + '">' + phone800 + '</a><br><a href="tel:' + phoneEkb.replace(/\D/g,'') + '">' + phoneEkb + '</a></div></div>';
|
||||
}
|
||||
contactsHtml += '<div class="hero-contact-item"><svg viewBox="0 0 24 24"><path d="M20 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 4l-8 5-8-5V6l8 5 8-5v2z"/></svg><a href="mailto:' + email + '">' + email + '</a></div>';
|
||||
contactsHtml += '<div class="hero-contact-item"><svg viewBox="0 0 24 24"><path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5A2.5 2.5 0 1112 6.5a2.5 2.5 0 010 5z"/></svg><span>' + address + '</span></div>';
|
||||
|
||||
var statsHtml = '';
|
||||
stats.forEach(function(s) {
|
||||
var val = s.is_dynamic == 1 ? yearsSince2011 : s.value;
|
||||
statsHtml += '<div class="hero-stat"><div class="hero-stat-number">' + val + '</div><div class="hero-stat-label">' + s.label + '</div></div>';
|
||||
});
|
||||
|
||||
var badgeHtml = h.badge_text ? '<div class="hero-badge"><svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2L15.09 8.26L22 9.27L17 14.14L18.18 21.02L12 17.77L5.82 21.02L7 14.14L2 9.27L8.91 8.26L12 2Z"/></svg> ' + h.badge_text + '</div>' : '';
|
||||
var h1text = (h.h1_text || 'демонтаж нефтегазопроводов по России, утилизация отходов').replace('{years}', yearsSince2011);
|
||||
|
||||
return badgeHtml +
|
||||
'<h1>' + h1text + '</h1>' +
|
||||
'<p class="hero-subtitle">' + (h.subtitle || '').replace('{years}', yearsSince2011) + '</p>' +
|
||||
'<div class="hero-contacts">' + contactsHtml + '</div>' +
|
||||
'<div class="hero-buttons">' +
|
||||
'<a href="' + (h.button1_link || '#contacts') + '" class="btn btn-primary">' + (h.button1_text || 'Оставить заявку') + '</a>' +
|
||||
'<a href="' + (h.button2_link || '#services') + '" class="btn btn-outline">' + (h.button2_text || 'Наши услуги') + '</a>' +
|
||||
'</div>' +
|
||||
'<div class="hero-stats">' + statsHtml + '</div>';
|
||||
}
|
||||
|
||||
function renderAbout(data) {
|
||||
var a = data.about || {};
|
||||
var features = data.about_features || [];
|
||||
var featsHtml = features.map(function(f) {
|
||||
return '<div class="about-feature"><div class="about-feature-icon"><svg viewBox="0 0 24 24">' + (f.svg_icon || '') + '</svg></div><div class="about-feature-text"><strong>' + f.title + '</strong>' + (f.description || '') + '</div></div>';
|
||||
}).join('');
|
||||
|
||||
return '<div class="section-header"><h2 class="section-title">О <span>компании</span></h2><p class="section-subtitle">' + (a.section_subtitle || '') + '</p></div>' +
|
||||
'<div class="about-grid">' +
|
||||
'<div class="about-image"><img src="' + imgSrc(a.about_image) + '" alt="" loading="lazy"></div>' +
|
||||
'<div class="about-text"><h3>' + (a.main_heading || '') + '</h3>' +
|
||||
(a.paragraph_1 ? '<p>' + a.paragraph_1 + '</p>' : '') +
|
||||
(a.paragraph_2 ? '<p>' + a.paragraph_2 + '</p>' : '') +
|
||||
(a.paragraph_3 ? '<p>' + a.paragraph_3 + '</p>' : '') +
|
||||
'<div class="about-features">' + featsHtml + '</div></div></div>';
|
||||
}
|
||||
|
||||
function renderServices(data) {
|
||||
var services = data.services || [];
|
||||
var st = data.settings || {};
|
||||
var cardsHtml = services.map(function(s) {
|
||||
var items = [];
|
||||
try { items = JSON.parse(s.items || '[]'); } catch(e) {}
|
||||
return '<div class="service-card' + (s.is_large == 1 ? ' large' : '') + '">' +
|
||||
'<div class="service-icon"><svg viewBox="0 0 24 24">' + (s.svg_icon || '') + '</svg></div>' +
|
||||
'<h3>' + s.title + '</h3><p>' + (s.description || '') + '</p>' +
|
||||
(items.length ? '<ul class="service-list">' + items.map(function(i) { return '<li>' + i + '</li>'; }).join('') + '</ul>' : '') +
|
||||
'</div>';
|
||||
}).join('');
|
||||
return '<div class="section-header center"><h2 class="section-title">Наши <span>услуги</span></h2><p class="section-subtitle">' + (st.section_services_subtitle || '') + '</p></div>' +
|
||||
'<div class="services-grid">' + cardsHtml + '</div>';
|
||||
}
|
||||
|
||||
function renderSchemes(data) {
|
||||
var schemes = data.work_schemes || [];
|
||||
var st = data.settings || {};
|
||||
var cardsHtml = schemes.map(function(s) {
|
||||
var items = [];
|
||||
try { items = JSON.parse(s.items || '[]'); } catch(e) {}
|
||||
return '<div class="service-card">' +
|
||||
'<div class="service-icon"><svg viewBox="0 0 24 24">' + (s.svg_icon || '') + '</svg></div>' +
|
||||
'<h3>' + s.title + '</h3><p>' + (s.description || '') + '</p>' +
|
||||
(items.length ? '<ul class="service-list">' + items.map(function(i) { return '<li>' + i + '</li>'; }).join('') + '</ul>' : '') +
|
||||
'</div>';
|
||||
}).join('');
|
||||
return '<div class="section-header center"><h2 class="section-title">Гибкие <span>схемы работы</span></h2><p class="section-subtitle">' + (st.section_schemes_subtitle || '') + '</p></div>' +
|
||||
'<div class="services-grid">' + cardsHtml + '</div>';
|
||||
}
|
||||
|
||||
function renderProcess(data) {
|
||||
var steps = data.process_steps || [];
|
||||
var st = data.settings || {};
|
||||
var stepsHtml = steps.map(function(s) {
|
||||
return '<div class="process-step"><div class="process-number">' + (s.step_number || s.sort_order + 1) + '</div><div class="process-content"><h3>' + s.title + '</h3><p>' + (s.description || '') + '</p></div></div>';
|
||||
}).join('');
|
||||
return '<div class="section-header center"><h2 class="section-title">Этапы <span>работы</span></h2><p class="section-subtitle">' + (st.section_process_subtitle || '') + '</p></div>' +
|
||||
'<div class="process-timeline">' + stepsHtml + '</div>';
|
||||
}
|
||||
|
||||
function renderEquipment(data) {
|
||||
var eq = data.equipment || [];
|
||||
var gallery = data.equipment_gallery || [];
|
||||
var st = data.settings || {};
|
||||
|
||||
function getImages(e) {
|
||||
var imgs = [];
|
||||
if (gallery.length) {
|
||||
gallery.filter(function(g) { return g.equipment_id === e.id; }).forEach(function(g) {
|
||||
if (g.image_path) imgs.push(g.image_path);
|
||||
});
|
||||
}
|
||||
if (imgs.length === 0 && e.image_path) imgs.push(e.image_path);
|
||||
return imgs;
|
||||
}
|
||||
|
||||
var cardsHtml = eq.map(function(e) {
|
||||
var specs = [];
|
||||
try { specs = JSON.parse(e.specs || '[]'); } catch(e2) {}
|
||||
var imgs = getImages(e);
|
||||
var imgHtml = imgs.length > 0
|
||||
? '<img src="' + imgSrc(imgs[0]) + '" loading="lazy" alt="' + esc(e.name) + '" style="width:100%;height:100%;object-fit:cover">'
|
||||
: '<div style="width:100%;height:100%;background:var(--bg-alt);display:flex;align-items:center;justify-content:center;color:var(--text-light)">Нет фото</div>';
|
||||
|
||||
return '<div class="equipment-card"><div class="equipment-card-image">' +
|
||||
imgHtml +
|
||||
'<div class="equip-overlay"><span class="brand">' + esc(e.brand || '') + '</span></div>' +
|
||||
'</div><div class="equipment-card-body"><h3>' + esc(e.name) + '</h3>' +
|
||||
(e.qty ? '<div class="equipment-qty">' + esc(e.qty) + '</div>' : '') +
|
||||
(specs.length ? '<div class="equipment-specs">' + specs.map(function(sp) { return '<div class="equipment-spec"><span class="equipment-spec-label">' + esc(sp.label) + '</span><span class="equipment-spec-value">' + esc(sp.value) + '</span></div>'; }).join('') + '</div>' : '') +
|
||||
'</div></div>';
|
||||
}).join('');
|
||||
|
||||
return '<div class="section-header center"><h2 class="section-title">Наша <span>спецтехника</span></h2><p class="section-subtitle">' + esc(st.section_equipment_subtitle || '') + '</p></div>' +
|
||||
'<div class="equipment-grid">' + cardsHtml + '</div>';
|
||||
}
|
||||
|
||||
function renderGeography(data) {
|
||||
var geo = data.geo || {};
|
||||
var regions = data.geo_regions || [];
|
||||
var st = data.settings || {};
|
||||
var regionsHtml = regions.map(function(r) {
|
||||
return '<div class="geo-region"><svg viewBox="0 0 24 24"><path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5A2.5 2.5 0 1112 6.5a2.5 2.5 0 010 5z"/></svg><span>' + (r.name || '') + '</span></div>';
|
||||
}).join('');
|
||||
return '<div class="section-header center"><h2 class="section-title">География <span>присутствия</span></h2><p class="section-subtitle">' + (geo.section_subtitle || st.section_services_subtitle || '') + '</p></div>' +
|
||||
'<div class="geo-content"><div><div class="geo-regions">' + regionsHtml + '</div>' +
|
||||
(geo.difficult_title ? '<div class="geo-hard"><h4>' + geo.difficult_title + '</h4><p>' + (geo.difficult_desc || '') + '</p></div>' : '') +
|
||||
'</div><div><div class="geo-map"><img src="' + imgSrc(geo.map_image) + '" loading="lazy" alt=""></div></div></div>';
|
||||
}
|
||||
|
||||
function renderLicenses(data) {
|
||||
var licenses = data.licenses || [];
|
||||
var gallery = data.license_gallery || [];
|
||||
var st = data.settings || {};
|
||||
var cardsHtml = licenses.map(function(l) {
|
||||
return '<div class="license-card"><div class="license-icon"><svg viewBox="0 0 24 24">' + (l.svg_icon || '') + '</svg></div><div><h4>' + l.title + '</h4><p>' + (l.description || '') + '</p></div></div>';
|
||||
}).join('');
|
||||
var galleryHtml = gallery.map(function(g) {
|
||||
return '<div class="license-gallery-item" data-src="' + imgSrc(g.image_path) + '" data-title="' + esc(g.title) + '" data-desc="' + esc(g.description || '') + '">' +
|
||||
'<div class="license-gallery-thumb"><img src="' + imgSrc(g.image_path) + '" loading="lazy" alt="' + esc(g.title) + '"><div class="license-gallery-zoom"><svg viewBox="0 0 24 24"><path d="M15.5 14h-.79l-.28-.27A6.471 6.471 0 0016 9.5 6.5 6.5 0 109.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/></svg></div></div>' +
|
||||
'<div class="license-gallery-body"><h4>' + g.title + '</h4><p>' + (g.description || '') + '</p></div></div>';
|
||||
}).join('');
|
||||
return '<div class="section-header center"><h2 class="section-title">Лицензии и <span>разрешения</span></h2><p class="section-subtitle">' + (st.section_licenses_subtitle || '') + '</p></div>' +
|
||||
'<div class="licenses-grid">' + cardsHtml + '</div>' +
|
||||
(gallery.length ? '<div class="license-gallery">' + galleryHtml + '</div>' : '');
|
||||
}
|
||||
|
||||
function renderActivity(data) {
|
||||
var acts = data.public_activities || [];
|
||||
var gallery = data.activity_gallery || [];
|
||||
var st = data.settings || {};
|
||||
var cardsHtml = acts.map(function(a) {
|
||||
return '<div class="activity-card"><div class="activity-card-icon"><svg viewBox="0 0 24 24">' + (a.svg_icon || '') + '</svg></div><h4>' + a.title + '</h4><p>' + (a.description || '') + '</p></div>';
|
||||
}).join('');
|
||||
var galleryHtml = gallery.length ? '<div class="activity-gallery"><div class="activity-gallery-title">Благодарственные письма</div>' +
|
||||
gallery.map(function(g) {
|
||||
return '<div class="license-gallery-item" data-src="' + imgSrc(g.image_path) + '" data-title="' + esc(g.title) + '" data-desc="' + esc(g.description || '') + '">' +
|
||||
'<div class="license-gallery-thumb"><img src="' + imgSrc(g.image_path) + '" loading="lazy" alt="' + esc(g.title) + '"><div class="license-gallery-zoom"><svg viewBox="0 0 24 24"><path d="M15.5 14h-.79l-.28-.27A6.471 6.471 0 0016 9.5 6.5 6.5 0 109.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/></svg></div></div>' +
|
||||
'<div class="license-gallery-body"><h4>' + g.title + '</h4><p>' + (g.description || '') + '</p></div></div>';
|
||||
}).join('') + '</div>' : '';
|
||||
return '<div class="section-header center"><h2 class="section-title">Общественная <span>деятельность</span></h2><p class="section-subtitle">' + (st.section_activity_subtitle || '') + '</p></div>' +
|
||||
'<div class="activity-cards">' + cardsHtml + '</div>' + galleryHtml;
|
||||
}
|
||||
|
||||
function renderPartners(data) {
|
||||
var partners = data.partners || [];
|
||||
var st = data.settings || {};
|
||||
var duplicated = partners.concat(partners);
|
||||
var trackHtml = duplicated.map(function(p) {
|
||||
return '<img src="' + imgSrc(p.logo_path) + '" loading="lazy" alt="' + esc(p.name || '') + '">';
|
||||
}).join('');
|
||||
return '<div class="section-header center"><h2 class="section-title">Наши <span>партнёры</span></h2><p class="section-subtitle">' + (st.section_partners_subtitle || '') + '</p></div>' +
|
||||
'<div class="partners-scroll"><div class="partners-track">' + trackHtml + '</div></div>';
|
||||
}
|
||||
|
||||
function renderContacts(data) {
|
||||
var s = data.settings || {};
|
||||
var h = data.hero || {};
|
||||
var phone800 = h.phone_800 || s.contact_phone_800 || '8 (800) 555-29-31';
|
||||
var phoneEkb = h.phone_ekb || s.contact_phone_ekb || '8 (343) 237-25-66';
|
||||
var email = h.email || s.contact_email || 'sts_zakaz@mail.ru';
|
||||
var address = h.address || s.contact_address || 'г. Екатеринбург, ул. Московская, 11, оф. 702';
|
||||
var workHours = (s.contact_work_hours || 'Пн–Пт: 9:00 – 18:00|Сб: 10:00 – 15:00').replace(/\|/g, '<br>');
|
||||
var mapImg = s.contact_map_image || './img/secret.webp';
|
||||
|
||||
return '<div class="section-header center"><h2 class="section-title">Свяжитесь <span>с нами</span></h2><p class="section-subtitle">' + (s.section_contacts_subtitle || '') + '</p></div>' +
|
||||
'<div class="contacts-grid"><div>' +
|
||||
'<div class="contact-info-list">' +
|
||||
'<div class="contact-info-item"><div class="contact-info-icon"><svg viewBox="0 0 24 24"><path d="M6.62 10.79a15.053 15.053 0 006.59 6.59l2.2-2.2a1 1 0 011.01-.24c1.12.37 2.33.57 3.58.57a1 1 0 011 1V20a1 1 0 01-1 1A17 17 0 013 4a1 1 0 011-1h3.5a1 1 0 011 1c0 1.25.2 2.46.57 3.58a1 1 0 01-.24 1.01l-2.2 2.2z"/></svg></div><div><h4>Телефон</h4><a href="tel:' + phone800.replace(/\D/g,'') + '">' + phone800 + '</a><br><a href="tel:' + phoneEkb.replace(/\D/g,'') + '">' + phoneEkb + '</a></div></div>' +
|
||||
'<div class="contact-info-item"><div class="contact-info-icon"><svg viewBox="0 0 24 24"><path d="M20 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 4l-8 5-8-5V6l8 5 8-5v2z"/></svg></div><div><h4>Email</h4><a href="mailto:' + email + '">' + email + '</a></div></div>' +
|
||||
'<div class="contact-info-item"><div class="contact-info-icon"><svg viewBox="0 0 24 24"><path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5A2.5 2.5 0 1112 6.5a2.5 2.5 0 010 5z"/></svg></div><div><h4>Адрес</h4><p>' + address + '</p></div></div>' +
|
||||
'<div class="contact-info-item"><div class="contact-info-icon"><svg viewBox="0 0 24 24"><path d="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8zm.5-13H11v6l5.25 3.15.75-1.23-4.5-2.67z"/></svg></div><div><h4>Режим работы</h4><p>' + workHours + '</p></div></div>' +
|
||||
'</div><div class="contact-map"><img src="' + imgSrc(mapImg) + '" loading="lazy" alt=""></div>' +
|
||||
'</div><div>' +
|
||||
'<div class="contact-form" id="contactForm"><h3>' + (s.form_title || 'Оставить заявку') + '</h3>' +
|
||||
'<p class="form-subtitle">' + (s.form_subtitle || 'Заполните форму и мы свяжемся с вами') + '</p>' +
|
||||
'<form id="feedbackForm" onsubmit="return handleFormSubmit(event)">' +
|
||||
'<div class="form-group"><label for="name">Ваше имя *</label><input type="text" id="name" name="name" placeholder="Иван Иванов" required></div>' +
|
||||
'<div class="form-group"><label for="phone">Телефон *</label><input type="tel" id="phone" name="phone" placeholder="+7 (___) ___-__-__" required></div>' +
|
||||
'<div class="form-group"><label for="message">Сообщение</label><textarea id="message" name="message" placeholder="Опишите ваш запрос..."></textarea></div>' +
|
||||
'<div class="form-group"><label class="form-consent-label"><input type="checkbox" name="consent" required id="consent"><span>Я согласен на <a href="/privacy.html" target="_blank">обработку персональных данных</a></span></label></div>' +
|
||||
'<button type="submit" class="btn btn-primary">Отправить заявку</button>' +
|
||||
'</form>' +
|
||||
'<div class="form-success" id="formSuccess"><svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg><h4>Заявка отправлена!</h4><p>Мы свяжемся с вами в ближайшее время</p></div>' +
|
||||
'</div></div></div>';
|
||||
}
|
||||
|
||||
function renderAll(data) {
|
||||
return {
|
||||
heroContent: renderHero(data),
|
||||
aboutContainer: renderAbout(data),
|
||||
servicesContainer: renderServices(data),
|
||||
schemesContainer: renderSchemes(data),
|
||||
processContainer: renderProcess(data),
|
||||
equipmentContainer: renderEquipment(data),
|
||||
geographyContainer: renderGeography(data),
|
||||
licensesContainer: renderLicenses(data),
|
||||
activityContainer: renderActivity(data),
|
||||
contactsContainer: renderContacts(data),
|
||||
partnersContainer: renderPartners(data)
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { renderAll };
|
||||
@@ -472,14 +472,13 @@ window.tab_equipment = function(tabId) {
|
||||
{field:'name',label:'Название'},
|
||||
{field:'brand',label:'Бренд'},
|
||||
{field:'qty',label:'Кол-во'},
|
||||
{field:'age_badge',label:'Возраст'},
|
||||
{field:'is_active',label:'Акт.',render:function(v){return v==1?'<span class="badge badge-success">Да</span>':'<span class="badge badge-warning">Нет</span>'}}
|
||||
];
|
||||
_fields = [
|
||||
{name:'name',label:'Название'},{name:'brand',label:'Бренд'},
|
||||
{name:'image_path',label:'Главное изображение',type:'image'},
|
||||
{name:'qty',label:'Количество'},{name:'specs',label:'Характеристики',type:'json'},
|
||||
{name:'age_badge',label:'Бейдж возраста'},{name:'sort_order',label:'Порядок',type:'number',default:'0'},
|
||||
{name:'sort_order',label:'Порядок',type:'number',default:'0'},
|
||||
{name:'is_active',label:'Активна',type:'number',default:'1'}
|
||||
];
|
||||
_uploadPrefix = 'equipment';
|
||||
@@ -814,6 +813,14 @@ window.tab_settings = function(tabId) {
|
||||
|
||||
document.getElementById('mainContent').innerHTML = h() +
|
||||
'<div class="settings-grid">' +
|
||||
'<div class="settings-card"><h3>Логотип сайта</h3>' +
|
||||
'<div id="logoPreview" style="margin-bottom:10px">' +
|
||||
(s.logo_path ? '<img src="' + s.logo_path + '" class="preview-img" style="max-height:80px">' : '<span class="text-muted">Логотип не загружен</span>') +
|
||||
'</div>' +
|
||||
'<input type="file" id="logoFileInput" accept=".jpg,.jpeg,.png,.webp,.svg,.gif,.bmp" style="margin-bottom:8px;width:100%">' +
|
||||
'<button class="btn btn-sm btn-primary" onclick="winUploadLogo()"><i class="fas fa-upload"></i> Загрузить логотип</button>' +
|
||||
'<p style="font-size:.75rem;color:var(--text-gray);margin-top:3px">При загрузке автоматически создаются favicon и apple-touch-icon</p>' +
|
||||
'</div>' +
|
||||
'<div class="settings-card"><h3>Контакты</h3>' +
|
||||
contactKeys.map(function(k) { return '<p style="margin-bottom:4px"><strong>' + k + ':</strong> <span class="text-muted">' + esc(String(s[k]||'').substring(0,80)) + '</span></p>'; }).join('') +
|
||||
'<button class="btn btn-sm btn-primary" style="margin-top:8px" onclick="winEditSettings(\'contacts\')"><i class="fas fa-edit"></i> Редактировать</button>' +
|
||||
@@ -825,6 +832,25 @@ window.tab_settings = function(tabId) {
|
||||
'</div>';
|
||||
|
||||
window._settings = s;
|
||||
window.winUploadLogo = async function() {
|
||||
var inp = document.getElementById('logoFileInput');
|
||||
if (!inp.files.length) { toast('Выберите файл','error'); return; }
|
||||
var fd = new FormData();
|
||||
fd.append('logo', inp.files[0]);
|
||||
try {
|
||||
var r = await fetch(BASE + '/settings/logo', {
|
||||
method: 'POST', headers: {'Authorization':'Bearer ' + token}, body: fd
|
||||
}).then(function(x) { return x.json(); });
|
||||
if (r.path) {
|
||||
toast('Логотип обновлён! Favicon сгенерированы');
|
||||
loadCurrentTab();
|
||||
} else {
|
||||
toast(r.error || 'Ошибка загрузки','error');
|
||||
}
|
||||
} catch(e) {
|
||||
toast('Ошибка загрузки','error');
|
||||
}
|
||||
};
|
||||
window.winEditSettings = function(mode) {
|
||||
var s = window._settings;
|
||||
var keys = mode === 'contacts' ? contactKeys : formKeys;
|
||||
|
||||
BIN
public/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
public/favicon-16x16.png
Normal file
|
After Width: | Height: | Size: 468 B |
BIN
public/favicon-32x32.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 710 KiB After Width: | Height: | Size: 115 KiB |
|
Before Width: | Height: | Size: 742 KiB After Width: | Height: | Size: 112 KiB |
|
Before Width: | Height: | Size: 875 KiB After Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 764 KiB After Width: | Height: | Size: 109 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 156 KiB |
|
Before Width: | Height: | Size: 787 KiB After Width: | Height: | Size: 200 KiB |
|
Before Width: | Height: | Size: 798 KiB After Width: | Height: | Size: 136 KiB |
|
Before Width: | Height: | Size: 744 KiB After Width: | Height: | Size: 130 KiB |
@@ -3,10 +3,55 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
||||
<title>СТС-Авто — Демонтаж нефтегазопроводов по России, утилизация отходов</title>
|
||||
<meta name="description" content="Демонтаж нефтегазопроводов по России, утилизация отходов. Собственная спецтехника. Лицензии СРО и НОСТРОЙ. Более {years} лет опыта.">
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700;800;900&family=Roboto+Condensed:wght@400;500;700&display=swap" rel="stylesheet">
|
||||
<title>Демонтаж нефтегазопроводов, утилизация отходов, рекультивация земель — СТС-Авто</title>
|
||||
<meta name="description" content="Более 15 лет опыта в демонтаже трубопроводов, утилизации промышленных отходов и рекультивации земель по всей России. Работаем с объектами любого масштаба.">
|
||||
<link rel="canonical" href="https://sts-avto.com/">
|
||||
<meta property="og:title" content="Демонтаж и утилизация — СТС-Авто">
|
||||
<meta property="og:description" content="Профессиональный демонтаж нефтегазопроводов и рекультивация земель.">
|
||||
<meta property="og:image" content="https://sts-avto.com/img/og-image.webp">
|
||||
<meta property="og:url" content="https://sts-avto.com/">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:locale" content="ru_RU">
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:title" content="Демонтаж и утилизация — СТС-Авто">
|
||||
<meta name="twitter:description" content="Профессиональный демонтаж нефтегазопроводов и рекультивация земель.">
|
||||
<meta name="twitter:image" content="https://sts-avto.com/img/og-image.webp">
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Organization",
|
||||
"name": "СТС-Авто",
|
||||
"url": "https://sts-avto.com/",
|
||||
"logo": "https://sts-avto.com/img/logo.webp",
|
||||
"address": {
|
||||
"@type": "PostalAddress",
|
||||
"addressLocality": "Екатеринбург",
|
||||
"streetAddress": "ул. Московская, 11, оф. 702"
|
||||
},
|
||||
"telephone": "+7-800-555-29-31",
|
||||
"areaServed": {
|
||||
"@type": "Country",
|
||||
"name": "Россия"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "BreadcrumbList",
|
||||
"itemListElement": [
|
||||
{ "@type": "ListItem", "position": 1, "name": "Главная", "item": "https://sts-avto.com/" },
|
||||
{ "@type": "ListItem", "position": 2, "name": "Услуги", "item": "https://sts-avto.com/#services" }
|
||||
]
|
||||
}
|
||||
</script>
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" media="print" onload="this.media='all'">
|
||||
<noscript><link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"></noscript>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700;800;900&family=Roboto+Condensed:wght@400;500;700&display=swap" rel="stylesheet" media="print" onload="this.media='all'">
|
||||
<noscript><link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700;800;900&family=Roboto+Condensed:wght@400;500;700&display=swap" rel="stylesheet"></noscript>
|
||||
<style>
|
||||
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
@@ -301,19 +346,6 @@ img { max-width: 100%; display: block; }
|
||||
}
|
||||
.equipment-card-body { padding: clamp(14px, 2.5vw, 22px); }
|
||||
.equipment-card-body h3 { font-size: 1.05rem; margin-bottom: clamp(10px, 1.5vw, 14px); color: var(--text-dark); }
|
||||
.equipment-age-badge {
|
||||
display: inline-flex; align-items: center; gap: clamp(4px, 0.8vw, 6px);
|
||||
background: rgba(39,174,96,0.1); border: 1px solid rgba(39,174,96,0.3);
|
||||
border-radius: 50px; padding: clamp(3px, 0.5vw, 4px) clamp(8px, 1.4vw, 12px); margin-bottom: clamp(8px, 1.4vw, 12px);
|
||||
font-size: clamp(10px, 1.2vw, 12px); font-weight: 700; color: #27AE60;
|
||||
font-family: 'Montserrat', sans-serif; text-transform: uppercase; letter-spacing: 0.5px;
|
||||
}
|
||||
.equipment-age-badge::before {
|
||||
content: '\2713';
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
width: 16px; height: 16px; background: #27AE60; color: #fff;
|
||||
border-radius: 50%; font-size: 10px;
|
||||
}
|
||||
.equipment-qty {
|
||||
font-size: clamp(12px, 1.5vw, 14px); font-weight: 700; color: var(--orange);
|
||||
margin-bottom: clamp(5px, 1vw, 8px); font-family: 'Montserrat', sans-serif;
|
||||
@@ -793,7 +825,7 @@ img { max-width: 100%; display: block; }
|
||||
<svg viewBox="0 0 24 24"><path d="M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z"/></svg>
|
||||
</button>
|
||||
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" defer></script>
|
||||
<script>
|
||||
(function() {
|
||||
'use strict';
|
||||
@@ -974,7 +1006,6 @@ img { max-width: 100%; display: block; }
|
||||
'<div class="equip-overlay"><span class="brand">' + esc(e.brand || '') + '</span></div>' +
|
||||
arrows + dots +
|
||||
'</div><div class="equipment-card-body"><h3>' + esc(e.name) + '</h3>' +
|
||||
(e.age_badge ? '<div class="equipment-age-badge">' + esc(e.age_badge) + '</div>' : '') +
|
||||
(e.qty ? '<div class="equipment-qty">' + esc(e.qty) + '</div>' : '') +
|
||||
(specs.length ? '<div class="equipment-specs">' + specs.map(function(sp) { return '<div class="equipment-spec"><span class="equipment-spec-label">' + esc(sp.label) + '</span><span class="equipment-spec-value">' + esc(sp.value) + '</span></div>'; }).join('') + '</div>' : '') +
|
||||
'</div></div>';
|
||||
@@ -1144,6 +1175,10 @@ img { max-width: 100%; display: block; }
|
||||
|
||||
function initSiteData(data) {
|
||||
SITE = data;
|
||||
if (data.settings && data.settings.logo_path) {
|
||||
var logoImg = document.querySelector('.logo img');
|
||||
if (logoImg) logoImg.src = data.settings.logo_path;
|
||||
}
|
||||
renderHero(data);
|
||||
renderAbout(data);
|
||||
renderServices(data);
|
||||
|
||||
6
public/robots.txt
Normal file
@@ -0,0 +1,6 @@
|
||||
User-agent: *
|
||||
Allow: /
|
||||
Disallow: /admin
|
||||
Disallow: /api/
|
||||
|
||||
Sitemap: https://sts-avto.com/sitemap.xml
|
||||
28
public/sitemap.xml
Normal file
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
<url>
|
||||
<loc>https://sts-avto.com/</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>1.0</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://sts-avto.com/privacy.html</loc>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.3</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://sts-avto.com/terms.html</loc>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.3</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://sts-avto.com/cookies.html</loc>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.3</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://sts-avto.com/requisites.html</loc>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.3</priority>
|
||||
</url>
|
||||
</urlset>
|
||||
54
server.js
@@ -6,6 +6,29 @@ const sharp = require('sharp');
|
||||
require('dotenv').config();
|
||||
|
||||
const app = express();
|
||||
|
||||
app.use((req, res, next) => {
|
||||
const proto = req.headers['x-forwarded-proto'];
|
||||
if (proto && proto !== 'https') {
|
||||
return res.redirect(301, 'https://' + req.headers.host + req.url);
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
app.use((req, res, next) => {
|
||||
const host = req.hostname;
|
||||
if (host.startsWith('www.')) {
|
||||
const target = host.replace(/^www\./, '');
|
||||
return res.redirect(301, 'https://' + target + req.originalUrl);
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
app.use((req, res, next) => {
|
||||
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');
|
||||
next();
|
||||
});
|
||||
|
||||
const PORT = process.env.PORT || 3000;
|
||||
const API_KEY = process.env.HOTEL777KEY;
|
||||
if (!API_KEY) {
|
||||
@@ -46,7 +69,16 @@ async function convertImagesToWebp() {
|
||||
}
|
||||
|
||||
app.use(express.json());
|
||||
app.use(express.static(path.join(__dirname, 'public')));
|
||||
|
||||
app.get('/robots.txt', (req, res) => {
|
||||
res.sendFile(path.join(__dirname, 'public', 'robots.txt'));
|
||||
});
|
||||
|
||||
app.get('/sitemap.xml', (req, res) => {
|
||||
res.sendFile(path.join(__dirname, 'public', 'sitemap.xml'));
|
||||
});
|
||||
|
||||
app.use(express.static(path.join(__dirname, 'public'), { index: false }));
|
||||
|
||||
const dataDir = path.join(__dirname, 'data');
|
||||
if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir);
|
||||
@@ -80,6 +112,7 @@ const settingsModule = require('./modules/settings');
|
||||
const usersModule = require('./modules/users');
|
||||
const leadsModule = require('./modules/leads');
|
||||
const siteDataModule = require('./modules/siteData');
|
||||
const ssrRenderer = require('./modules/ssrRenderer');
|
||||
|
||||
[
|
||||
crudModule, authModule, heroModule, aboutModule,
|
||||
@@ -114,11 +147,22 @@ app.get('/admin', (req, res) => {
|
||||
res.sendFile(path.join(__dirname, 'public', 'admin.html'));
|
||||
});
|
||||
|
||||
app.get('/', (req, res) => {
|
||||
const years = new Date().getFullYear() - 2011;
|
||||
app.get('/', async (req, res) => {
|
||||
const htmlPath = path.join(__dirname, 'public', 'index.html');
|
||||
const html = fs.readFileSync(htmlPath, 'utf8');
|
||||
res.send(html.replace(/\{years\}/g, String(years)));
|
||||
let html = fs.readFileSync(htmlPath, 'utf8');
|
||||
|
||||
try {
|
||||
const siteData = await siteDataModule.loadSiteData();
|
||||
const ssrHtml = ssrRenderer.renderAll(siteData);
|
||||
for (const [containerId, content] of Object.entries(ssrHtml)) {
|
||||
const placeholder = 'id="' + containerId + '">';
|
||||
html = html.replace(placeholder, placeholder + content);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('SSR render error:', err.message);
|
||||
}
|
||||
|
||||
res.send(html);
|
||||
});
|
||||
|
||||
convertImagesToWebp().then(() => console.log('Images ready'));
|
||||
|
||||