v260513
13
.gitignore
vendored
@@ -1,13 +1,4 @@
|
|||||||
.env
|
|
||||||
node_modules
|
node_modules
|
||||||
git
|
|
||||||
promt
|
|
||||||
/node_modules/
|
|
||||||
/node_modules*
|
|
||||||
*.log
|
|
||||||
package-lock.json
|
package-lock.json
|
||||||
data
|
.env
|
||||||
data
|
data
|
||||||
promt
|
|
||||||
LICENSE
|
|
||||||
25.xlsx
|
|
||||||
120
README.md
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
# СТС-Авто — Демонтаж и капитальный ремонт трубопроводов
|
||||||
|
|
||||||
|
Сайт компании «СТС-Авто». Фронтенд на чистом HTML/CSS/JS, бэкенд на Node.js + Express + SQLite.
|
||||||
|
|
||||||
|
## Запуск
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
Откроется на http://localhost:3000
|
||||||
|
|
||||||
|
## Переменные окружения
|
||||||
|
|
||||||
|
Создайте файл `.env`:
|
||||||
|
|
||||||
|
```
|
||||||
|
HOTEL777KEY=ваш_секретный_ключ
|
||||||
|
PORT=3000
|
||||||
|
```
|
||||||
|
|
||||||
|
## REST API
|
||||||
|
|
||||||
|
### Создать заявку
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /api/leads
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "Иван Иванов",
|
||||||
|
"phone": "+7 (999) 123-45-67",
|
||||||
|
"message": "Текст сообщения"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Ответ:**
|
||||||
|
```json
|
||||||
|
{ "id": 1, "message": "Lead saved" }
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Получить список заявок
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/leads
|
||||||
|
X-API-Key: HFwy+tfAljHEq8R21BCRt+Ps4SN65bu8zFagA68N24s
|
||||||
|
```
|
||||||
|
|
||||||
|
**Ответ:**
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "Иван Иванов",
|
||||||
|
"phone": "+7 (999) 123-45-67",
|
||||||
|
"message": "Текст сообщения",
|
||||||
|
"created_at": "2026-05-13 15:30:00"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Удалить заявку
|
||||||
|
|
||||||
|
```
|
||||||
|
DELETE /api/leads/:id
|
||||||
|
X-API-Key: HFwy+tfAljHEq8R21BCRt+Ps4SN65bu8zFagA68N24s
|
||||||
|
```
|
||||||
|
|
||||||
|
**Ответ:**
|
||||||
|
```json
|
||||||
|
{ "message": "Lead deleted" }
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Примеры curl (Windows PowerShell)
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Создать заявку
|
||||||
|
$body = @{ name="Иван"; phone="+7 (999) 123-45-67"; message="Тест" } | ConvertTo-Json
|
||||||
|
Invoke-RestMethod -Uri "http://localhost:3000/api/leads" -Method Post -ContentType "application/json" -Body $body
|
||||||
|
|
||||||
|
# Получить заявки
|
||||||
|
$headers = @{ "X-API-Key" = "HFwy+tfAljHEq8R21BCRt+Ps4SN65bu8zFagA68N24s" }
|
||||||
|
Invoke-RestMethod -Uri "http://localhost:3000/api/leads" -Method Get -Headers $headers
|
||||||
|
|
||||||
|
# Удалить заявку
|
||||||
|
Invoke-RestMethod -Uri "http://localhost:3000/api/leads/1" -Method Delete -Headers $headers
|
||||||
|
```
|
||||||
|
|
||||||
|
## Структура проекта
|
||||||
|
|
||||||
|
```
|
||||||
|
sts-avto/
|
||||||
|
├── public/
|
||||||
|
│ ├── index.html # Главная страница
|
||||||
|
│ └── img/ # Изображения (.webp)
|
||||||
|
├── data/
|
||||||
|
│ └── bookings.db # SQLite база данных
|
||||||
|
├── server.js # Express сервер
|
||||||
|
├── .env # Переменные окружения
|
||||||
|
└── package.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## База данных
|
||||||
|
|
||||||
|
Хранится в `data/bookings.db` (SQLite). Таблица `leads`:
|
||||||
|
|
||||||
|
| Поле | Тип | Описание |
|
||||||
|
|------|-----|---------|
|
||||||
|
| id | INTEGER | PK, autoincrement |
|
||||||
|
| name | TEXT | Имя клиента |
|
||||||
|
| phone | TEXT | Телефон |
|
||||||
|
| message | TEXT | Сообщение |
|
||||||
|
| created_at | DATETIME | Дата создания |
|
||||||
12
package.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"dependencies": {
|
||||||
|
"dotenv": "^17.4.2",
|
||||||
|
"express": "^5.2.1",
|
||||||
|
"sharp": "^0.34.5",
|
||||||
|
"sqlite3": "^6.0.1"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"start": "node server.js",
|
||||||
|
"dev": "nodemon server.js"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
public/img/belarus-mtz1523.webp
Normal file
|
After Width: | Height: | Size: 144 KiB |
BIN
public/img/caterpillar-336.webp
Normal file
|
After Width: | Height: | Size: 172 KiB |
BIN
public/img/geo.webp
Normal file
|
After Width: | Height: | Size: 295 KiB |
BIN
public/img/hero-bg.webp
Normal file
|
After Width: | Height: | Size: 154 KiB |
BIN
public/img/hitachi-zx350.webp
Normal file
|
After Width: | Height: | Size: 170 KiB |
BIN
public/img/howo-sitrak.webp
Normal file
|
After Width: | Height: | Size: 154 KiB |
BIN
public/img/ivanovets-ks5571.webp
Normal file
|
After Width: | Height: | Size: 157 KiB |
BIN
public/img/john-deere.webp
Normal file
|
After Width: | Height: | Size: 116 KiB |
BIN
public/img/kamaz-6520.webp
Normal file
|
After Width: | Height: | Size: 130 KiB |
BIN
public/img/komatsu-pc400.webp
Normal file
|
After Width: | Height: | Size: 179 KiB |
BIN
public/img/ks-45721.webp
Normal file
|
After Width: | Height: | Size: 144 KiB |
BIN
public/img/license-lom.webp
Normal file
|
After Width: | Height: | Size: 514 KiB |
BIN
public/img/license-otxody.webp
Normal file
|
After Width: | Height: | Size: 541 KiB |
BIN
public/img/logo.webp
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
public/img/secret.webp
Normal file
|
After Width: | Height: | Size: 164 KiB |
BIN
public/img/tral-1.webp
Normal file
|
After Width: | Height: | Size: 112 KiB |
BIN
public/img/tral-2.webp
Normal file
|
After Width: | Height: | Size: 110 KiB |
1329
public/index.html
Normal file
141
server.js
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
|
const sqlite3 = require('sqlite3').verbose();
|
||||||
|
const sharp = require('sharp');
|
||||||
|
require('dotenv').config();
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
const PORT = process.env.PORT || 3000;
|
||||||
|
const API_KEY = process.env.HOTEL777KEY;
|
||||||
|
if (!API_KEY) {
|
||||||
|
console.error('FATAL: HOTEL777KEY environment variable not set');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const imgDir = path.join(__dirname, 'public', 'img');
|
||||||
|
const supportedFormats = ['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.tiff'];
|
||||||
|
|
||||||
|
async function convertImagesToWebp() {
|
||||||
|
if (!fs.existsSync(imgDir)) {
|
||||||
|
console.log('📁 Папка img не найдена, пропускаем конвертацию');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const files = fs.readdirSync(imgDir);
|
||||||
|
const toConvert = files.filter(f => {
|
||||||
|
const ext = path.extname(f).toLowerCase();
|
||||||
|
return supportedFormats.includes(ext) && !f.endsWith('.webp');
|
||||||
|
});
|
||||||
|
|
||||||
|
if (toConvert.length === 0) {
|
||||||
|
console.log('✅ Все изображения уже в формате WebP');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`🔄 Конвертация ${toConvert.length} изображений в WebP...`);
|
||||||
|
|
||||||
|
for (const file of toConvert) {
|
||||||
|
const inputPath = path.join(imgDir, file);
|
||||||
|
const outputPath = path.join(imgDir, path.basename(file, path.extname(file)) + '.webp');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await sharp(inputPath)
|
||||||
|
.webp({ quality: 85 })
|
||||||
|
.toFile(outputPath);
|
||||||
|
fs.unlinkSync(inputPath);
|
||||||
|
console.log(` ✅ ${file} → ${path.basename(outputPath)}`);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(` ❌ Ошибка конвертации ${file}:`, err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('✅ Конвертация завершена');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Middleware
|
||||||
|
app.use(express.json());
|
||||||
|
app.use(express.static(path.join(__dirname, 'public')));
|
||||||
|
|
||||||
|
// Ensure data directory and database
|
||||||
|
const dataDir = path.join(__dirname, 'data');
|
||||||
|
if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir);
|
||||||
|
const dbPath = path.join(dataDir, 'bookings.db');
|
||||||
|
const db = new sqlite3.Database(dbPath);
|
||||||
|
|
||||||
|
db.run(`CREATE TABLE IF NOT EXISTS leads (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
phone TEXT NOT NULL,
|
||||||
|
message TEXT,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)`);
|
||||||
|
|
||||||
|
// API: POST /api/leads – сохранить новую заявку (публичный)
|
||||||
|
app.post('/api/leads', (req, res) => {
|
||||||
|
const { name, phone, message } = req.body;
|
||||||
|
if (!name || !phone) {
|
||||||
|
return res.status(400).json({ error: 'Missing required fields: name, phone' });
|
||||||
|
}
|
||||||
|
const stmt = db.prepare(`INSERT INTO leads (name, phone, message) VALUES (?, ?, ?)`);
|
||||||
|
stmt.run(name.trim(), phone.trim(), (message || '').trim(), function(err) {
|
||||||
|
if (err) {
|
||||||
|
console.error(err);
|
||||||
|
return res.status(500).json({ error: 'Database error' });
|
||||||
|
}
|
||||||
|
console.log(`✅ Новая заявка #${this.lastID}: ${name} | ${phone}`);
|
||||||
|
res.status(201).json({ id: this.lastID, message: 'Lead saved' });
|
||||||
|
});
|
||||||
|
stmt.finalize();
|
||||||
|
});
|
||||||
|
|
||||||
|
// API: GET /api/leads – получить список всех заявок (требуется API-ключ)
|
||||||
|
app.get('/api/leads', (req, res) => {
|
||||||
|
const providedKey = req.headers['x-api-key'];
|
||||||
|
if (!providedKey || providedKey !== API_KEY) {
|
||||||
|
return res.status(401).json({ error: 'Invalid or missing API key' });
|
||||||
|
}
|
||||||
|
db.all(`SELECT id, name, phone, message, created_at FROM leads ORDER BY created_at DESC`, (err, rows) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(err);
|
||||||
|
return res.status(500).json({ error: 'Database error' });
|
||||||
|
}
|
||||||
|
res.json(rows);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// API: DELETE /api/leads/:id – удалить заявку (требуется API-ключ)
|
||||||
|
app.delete('/api/leads/:id', (req, res) => {
|
||||||
|
const providedKey = req.headers['x-api-key'];
|
||||||
|
if (!providedKey || providedKey !== API_KEY) {
|
||||||
|
return res.status(401).json({ error: 'Invalid or missing API key' });
|
||||||
|
}
|
||||||
|
const id = parseInt(req.params.id);
|
||||||
|
if (isNaN(id)) {
|
||||||
|
return res.status(400).json({ error: 'Invalid ID' });
|
||||||
|
}
|
||||||
|
const stmt = db.prepare(`DELETE FROM leads WHERE id = ?`);
|
||||||
|
stmt.run(id, function(err) {
|
||||||
|
if (err) {
|
||||||
|
console.error(err);
|
||||||
|
return res.status(500).json({ error: 'Database error' });
|
||||||
|
}
|
||||||
|
if (this.changes === 0) {
|
||||||
|
return res.status(404).json({ error: 'Lead not found' });
|
||||||
|
}
|
||||||
|
console.log(`🗑️ Удалена заявка #${id}`);
|
||||||
|
res.json({ message: 'Lead deleted' });
|
||||||
|
});
|
||||||
|
stmt.finalize();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Serve frontend
|
||||||
|
app.get('/', (req, res) => {
|
||||||
|
res.sendFile(path.join(__dirname, 'public', 'index.html'));
|
||||||
|
});
|
||||||
|
|
||||||
|
app.listen(PORT, async () => {
|
||||||
|
await convertImagesToWebp();
|
||||||
|
console.log(`✅ server running on http://localhost:${PORT}`);
|
||||||
|
console.log(`🔑 API Key: ${API_KEY}`);
|
||||||
|
});
|
||||||