ntp&modercontent_front
This commit is contained in:
@@ -30,6 +30,7 @@ function Editor() {
|
|||||||
}, 2200);
|
}, 2200);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
// Fetch CSRF token
|
// Fetch CSRF token
|
||||||
//useEffect(() => {
|
//useEffect(() => {
|
||||||
// fetch('/moders/csrf-token')
|
// fetch('/moders/csrf-token')
|
||||||
@@ -256,8 +257,18 @@ const authenticatedFetch = useCallback((url, options = {}) => {
|
|||||||
<button id="logout-btn" style={{ float: 'right' }} onClick={handleLogout}>Выйти из системы</button>
|
<button id="logout-btn" style={{ float: 'right' }} onClick={handleLogout}>Выйти из системы</button>
|
||||||
<h2>Редактирование вкладок</h2>
|
<h2>Редактирование вкладок</h2>
|
||||||
<div className="tab-controls">
|
<div className="tab-controls">
|
||||||
<button className={`tab-btn ${activeTab === 'info' ? 'active' : ''}`} onClick={() => handleTabChange('info')} data-tab="info">Общая информация</button>
|
<button className={`tab-btn ${activeTab === 'info' ? 'active' : ''}`} onClick={() => handleTabChange('info')} data-tab="info">
|
||||||
<button className={`tab-btn ${activeTab === 'union' ? 'active' : ''}`} onClick={() => handleTabChange('union')} data-tab="union">Профсоюз</button>
|
Общая информация
|
||||||
|
</button>
|
||||||
|
<button className={`tab-btn ${activeTab === 'events' ? 'active' : ''}`} onClick={() => handleTabChange('events')} data-tab="events">
|
||||||
|
📅 Мероприятия
|
||||||
|
</button>
|
||||||
|
<button className={`tab-btn ${activeTab === 'help' ? 'active' : ''}`} onClick={() => handleTabChange('help')} data-tab="help">
|
||||||
|
💰 Материальная помощь
|
||||||
|
</button>
|
||||||
|
<button className={`tab-btn ${activeTab === 'housing' ? 'active' : ''}`} onClick={() => handleTabChange('housing')} data-tab="housing">
|
||||||
|
🏠 Жилищные условия
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ margin: '18px 0' }}>
|
<div style={{ margin: '18px 0' }}>
|
||||||
<button id="add-block-btn" onClick={handleAddBlock}>Добавить блок</button>
|
<button id="add-block-btn" onClick={handleAddBlock}>Добавить блок</button>
|
||||||
|
|||||||
@@ -1658,3 +1658,96 @@
|
|||||||
[2026-08-10T07:18:02.945Z] [INFO] [LOGIN] Ответ отправлен
|
[2026-08-10T07:18:02.945Z] [INFO] [LOGIN] Ответ отправлен
|
||||||
[2026-08-10T07:22:25.057Z] [INFO] Server running on http://localhost:3005
|
[2026-08-10T07:22:25.057Z] [INFO] Server running on http://localhost:3005
|
||||||
[2026-08-10T07:22:25.060Z] [INFO] Card logs initialized successfully
|
[2026-08-10T07:22:25.060Z] [INFO] Card logs initialized successfully
|
||||||
|
[2026-08-10T17:21:24.568Z] [INFO] Server running on http://localhost:3005
|
||||||
|
[2026-08-10T17:21:24.572Z] [INFO] Card logs initialized successfully
|
||||||
|
[2026-08-10T17:38:05.413Z] [INFO] Server running on http://localhost:3005
|
||||||
|
[2026-08-10T17:38:05.416Z] [INFO] Card logs initialized successfully
|
||||||
|
[2026-08-10T17:38:24.712Z] [INFO] [LOGIN] Попытка входа: grivova
|
||||||
|
[2026-08-10T17:38:24.713Z] [INFO] [LOGIN] Проверка подключения к базе...
|
||||||
|
[2026-08-10T17:38:24.749Z] [INFO] [LOGIN] Результат поиска пользователя: не найден
|
||||||
|
[2026-08-10T17:38:24.750Z] [WARN] [LOGIN] Пользователь не найден: grivova
|
||||||
|
[2026-08-10T17:38:29.893Z] [INFO] [LOGIN] Попытка входа: vova
|
||||||
|
[2026-08-10T17:38:29.894Z] [INFO] [LOGIN] Проверка подключения к базе...
|
||||||
|
[2026-08-10T17:38:29.897Z] [INFO] [LOGIN] Результат поиска пользователя: найден
|
||||||
|
[2026-08-10T17:38:29.898Z] [INFO] [LOGIN] Сравнение пароля через bcrypt...
|
||||||
|
[2026-08-10T17:38:30.033Z] [INFO] [LOGIN] Результат сравнения пароля: true
|
||||||
|
[2026-08-10T17:38:30.034Z] [INFO] [LOGIN] Сессия установлена: {"isModerator":true,"userId":4,"username":"vova"}
|
||||||
|
[2026-08-10T17:38:30.038Z] [INFO] [LOGIN] Ответ отправлен
|
||||||
|
[2026-08-10T17:42:06.235Z] [INFO] Server running on http://localhost:3005
|
||||||
|
[2026-08-10T17:42:06.238Z] [INFO] Card logs initialized successfully
|
||||||
|
[2026-08-10T17:43:18.054Z] [INFO] [DB CHECK] Проверка подключения к MySQL: localhost:3306/rasch_content
|
||||||
|
[2026-08-10T17:43:18.068Z] [INFO] [DB CHECK] ✅ Успешное подключение к MySQL
|
||||||
|
[2026-08-10T17:43:42.744Z] [INFO] [LOGIN] Попытка входа: vova
|
||||||
|
[2026-08-10T17:43:42.745Z] [INFO] [LOGIN] Проверка подключения к базе...
|
||||||
|
[2026-08-10T17:43:42.751Z] [INFO] [LOGIN] Результат поиска пользователя: найден
|
||||||
|
[2026-08-10T17:43:42.752Z] [INFO] [LOGIN] Сравнение пароля через bcrypt...
|
||||||
|
[2026-08-10T17:43:42.833Z] [INFO] [LOGIN] Результат сравнения пароля: true
|
||||||
|
[2026-08-10T17:43:42.834Z] [INFO] [LOGIN] Сессия установлена: {"isModerator":true,"userId":4,"username":"vova"}
|
||||||
|
[2026-08-10T17:43:42.835Z] [INFO] [LOGIN] Ответ отправлен
|
||||||
|
[2026-08-10T17:44:32.453Z] [INFO] [DB CHECK] Проверка подключения к MySQL: localhost:3306/rasch_content
|
||||||
|
[2026-08-10T17:44:32.465Z] [INFO] [DB CHECK] ✅ Успешное подключение к MySQL
|
||||||
|
[2026-08-10T17:44:38.654Z] [INFO] [LOGIN] Попытка входа: vova
|
||||||
|
[2026-08-10T17:44:38.655Z] [INFO] [LOGIN] Проверка подключения к базе...
|
||||||
|
[2026-08-10T17:44:38.657Z] [INFO] [LOGIN] Результат поиска пользователя: найден
|
||||||
|
[2026-08-10T17:44:38.658Z] [INFO] [LOGIN] Сравнение пароля через bcrypt...
|
||||||
|
[2026-08-10T17:44:38.794Z] [INFO] [LOGIN] Результат сравнения пароля: true
|
||||||
|
[2026-08-10T17:44:38.795Z] [INFO] [LOGIN] Сессия установлена: {"isModerator":true,"userId":4,"username":"vova"}
|
||||||
|
[2026-08-10T17:44:38.798Z] [INFO] [LOGIN] Ответ отправлен
|
||||||
|
[2026-08-10T17:49:04.372Z] [INFO] Server running on http://localhost:3005
|
||||||
|
[2026-08-10T17:49:04.376Z] [INFO] Card logs initialized successfully
|
||||||
|
[2026-08-10T18:07:19.446Z] [INFO] Server running on http://localhost:3005
|
||||||
|
[2026-08-10T18:07:19.450Z] [INFO] Card logs initialized successfully
|
||||||
|
[2026-08-10T18:07:21.917Z] [INFO] [DB CHECK] Проверка подключения к MySQL: localhost:3306/rasch_content
|
||||||
|
[2026-08-10T18:07:21.973Z] [INFO] [DB CHECK] ✅ Успешное подключение к MySQL
|
||||||
|
[2026-08-10T18:07:22.009Z] [ERROR] [DB CHECK] ❌ Ошибка связи с бэкендом: connect ECONNREFUSED 127.0.0.1:3001; connect ECONNREFUSED ::1:3001
|
||||||
|
[2026-08-10T18:09:04.717Z] [INFO] Server running on http://localhost:3005
|
||||||
|
[2026-08-10T18:09:04.720Z] [INFO] Card logs initialized successfully
|
||||||
|
[2026-08-10T18:11:04.542Z] [INFO] Server running on http://localhost:3005
|
||||||
|
[2026-08-10T18:11:04.545Z] [INFO] Card logs initialized successfully
|
||||||
|
[2026-08-10T19:48:50.904Z] [INFO] Server running on http://localhost:3005
|
||||||
|
[2026-08-10T19:48:50.908Z] [INFO] Card logs initialized successfully
|
||||||
|
[2026-08-10T19:49:05.177Z] [INFO] [DB CHECK] Проверка подключения к MySQL: localhost:3306/rasch_content
|
||||||
|
[2026-08-10T19:49:05.244Z] [INFO] [DB CHECK] ✅ Успешное подключение к MySQL
|
||||||
|
[2026-08-10T19:49:05.309Z] [ERROR] [DB CHECK] ❌ Ошибка связи с бэкендом: connect ECONNREFUSED 127.0.0.1:3001; connect ECONNREFUSED ::1:3001
|
||||||
|
[2026-08-10T19:52:24.799Z] [INFO] Server running on http://localhost:3005
|
||||||
|
[2026-08-10T19:52:24.804Z] [INFO] Card logs initialized successfully
|
||||||
|
[2026-08-10T19:54:26.429Z] [INFO] [LOGIN] Попытка входа: vova
|
||||||
|
[2026-08-10T19:54:26.430Z] [INFO] [LOGIN] Проверка подключения к базе...
|
||||||
|
[2026-08-10T19:54:26.437Z] [INFO] [LOGIN] Результат поиска пользователя: найден
|
||||||
|
[2026-08-10T19:54:26.439Z] [INFO] [LOGIN] Сравнение пароля через bcrypt...
|
||||||
|
[2026-08-10T19:54:26.774Z] [INFO] [LOGIN] Результат сравнения пароля: true
|
||||||
|
[2026-08-10T19:54:26.776Z] [INFO] [LOGIN] Сессия установлена: {"isModerator":true,"userId":4,"username":"vova"}
|
||||||
|
[2026-08-10T19:54:26.780Z] [INFO] [LOGIN] Ответ отправлен
|
||||||
|
[2026-08-10T19:57:35.827Z] [INFO] [DB CHECK] Проверка подключения к MySQL: localhost:3306/rasch_content
|
||||||
|
[2026-08-10T19:57:35.848Z] [INFO] [DB CHECK] ✅ Успешное подключение к MySQL
|
||||||
|
[2026-08-10T20:00:31.524Z] [ERROR] PayloadTooLargeError: request entity too large
|
||||||
|
at readStream (D:\rasch\admin-server1\node_modules\raw-body\index.js:163:17)
|
||||||
|
at getRawBody (D:\rasch\admin-server1\node_modules\raw-body\index.js:116:12)
|
||||||
|
at read (D:\rasch\admin-server1\node_modules\body-parser\lib\read.js:113:3)
|
||||||
|
at jsonParser (D:\rasch\admin-server1\node_modules\body-parser\lib\types\json.js:60:5)
|
||||||
|
at Layer.handleRequest (D:\rasch\admin-server1\node_modules\router\lib\layer.js:152:17)
|
||||||
|
at trimPrefix (D:\rasch\admin-server1\node_modules\router\index.js:342:13)
|
||||||
|
at D:\rasch\admin-server1\node_modules\router\index.js:297:9
|
||||||
|
at processParams (D:\rasch\admin-server1\node_modules\router\index.js:582:12)
|
||||||
|
at next (D:\rasch\admin-server1\node_modules\router\index.js:291:5)
|
||||||
|
at logger (D:\rasch\admin-server1\node_modules\morgan\index.js:170:5)
|
||||||
|
[2026-08-10T20:02:19.620Z] [INFO] Server running on http://localhost:3005
|
||||||
|
[2026-08-10T20:02:19.623Z] [INFO] Card logs initialized successfully
|
||||||
|
[2026-08-10T20:05:33.895Z] [INFO] Server running on http://localhost:3005
|
||||||
|
[2026-08-10T20:05:33.899Z] [INFO] Card logs initialized successfully
|
||||||
|
[2026-08-10T20:08:11.744Z] [INFO] Server running on http://localhost:3005
|
||||||
|
[2026-08-10T20:08:11.746Z] [INFO] Card logs initialized successfully
|
||||||
|
[2026-08-10T20:09:55.295Z] [INFO] Server running on http://localhost:3005
|
||||||
|
[2026-08-10T20:09:55.298Z] [INFO] Card logs initialized successfully
|
||||||
|
[2026-08-10T20:11:38.925Z] [INFO] Server running on http://localhost:3005
|
||||||
|
[2026-08-10T20:11:38.928Z] [INFO] Card logs initialized successfully
|
||||||
|
[2026-08-10T20:13:11.668Z] [INFO] Server running on http://localhost:3005
|
||||||
|
[2026-08-10T20:13:11.671Z] [INFO] Card logs initialized successfully
|
||||||
|
[2026-08-10T20:13:47.030Z] [INFO] Server running on http://localhost:3005
|
||||||
|
[2026-08-10T20:13:47.033Z] [INFO] Card logs initialized successfully
|
||||||
|
[2026-08-10T20:17:59.208Z] [INFO] Server running on http://localhost:3005
|
||||||
|
[2026-08-10T20:17:59.211Z] [INFO] Card logs initialized successfully
|
||||||
|
[2026-08-10T20:18:33.110Z] [INFO] Server running on http://localhost:3005
|
||||||
|
[2026-08-10T20:18:33.113Z] [INFO] Card logs initialized successfully
|
||||||
|
[2026-08-10T20:24:20.748Z] [INFO] Server running on http://localhost:3005
|
||||||
|
[2026-08-10T20:24:20.752Z] [INFO] Card logs initialized successfully
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ const upload = multer({
|
|||||||
// Получить все блоки для вкладки
|
// Получить все блоки для вкладки
|
||||||
router.get('/:tab', async (req, res) => {
|
router.get('/:tab', async (req, res) => {
|
||||||
const { tab } = req.params;
|
const { tab } = req.params;
|
||||||
if (!['info', 'union'].includes(tab)) return res.status(400).json({ message: 'Некорректная вкладка' });
|
if (!['info', 'union', 'events', 'help', 'housing'].includes(tab)) return res.status(400).json({ message: 'Некорректная вкладка' });
|
||||||
try {
|
try {
|
||||||
const [rows] = await db.query('SELECT * FROM content_blocks WHERE tab = ? ORDER BY created_at ASC', [tab]);
|
const [rows] = await db.query('SELECT * FROM content_blocks WHERE tab = ? ORDER BY created_at ASC', [tab]);
|
||||||
res.json(rows);
|
res.json(rows);
|
||||||
@@ -57,7 +57,7 @@ router.post('/:tab/upload', requireModerator, upload.single('file'), (req, res)
|
|||||||
router.post('/:tab', requireModerator, async (req, res) => {
|
router.post('/:tab', requireModerator, async (req, res) => {
|
||||||
const { tab } = req.params;
|
const { tab } = req.params;
|
||||||
const { content } = req.body;
|
const { content } = req.body;
|
||||||
if (!['info', 'union'].includes(tab)) return res.status(400).json({ message: 'Некорректная вкладка' });
|
if (!['info', 'union', 'events', 'help', 'housing'].includes(tab)) return res.status(400).json({ message: 'Некорректная вкладка' });
|
||||||
try {
|
try {
|
||||||
const [result] = await db.query(
|
const [result] = await db.query(
|
||||||
'INSERT INTO content_blocks (tab, type, content, created_by, created_at, updated_at) VALUES (?, ?, ?, ?, NOW(), NOW())',
|
'INSERT INTO content_blocks (tab, type, content, created_by, created_at, updated_at) VALUES (?, ?, ?, ?, NOW(), NOW())',
|
||||||
@@ -73,7 +73,7 @@ router.post('/:tab', requireModerator, async (req, res) => {
|
|||||||
router.put('/:tab/:id', requireModerator, async (req, res) => {
|
router.put('/:tab/:id', requireModerator, async (req, res) => {
|
||||||
const { tab, id } = req.params;
|
const { tab, id } = req.params;
|
||||||
const { content } = req.body;
|
const { content } = req.body;
|
||||||
if (!['info', 'union'].includes(tab)) return res.status(400).json({ message: 'Некорректная вкладка' });
|
if (!['info', 'union', 'events', 'help', 'housing'].includes(tab)) return res.status(400).json({ message: 'Некорректная вкладка' });
|
||||||
try {
|
try {
|
||||||
await db.query(
|
await db.query(
|
||||||
'UPDATE content_blocks SET content = ?, updated_at = NOW() WHERE id = ? AND tab = ?',
|
'UPDATE content_blocks SET content = ?, updated_at = NOW() WHERE id = ? AND tab = ?',
|
||||||
@@ -88,7 +88,7 @@ router.put('/:tab/:id', requireModerator, async (req, res) => {
|
|||||||
// Удалить блок
|
// Удалить блок
|
||||||
router.delete('/:tab/:id', requireModerator, async (req, res) => {
|
router.delete('/:tab/:id', requireModerator, async (req, res) => {
|
||||||
const { tab, id } = req.params;
|
const { tab, id } = req.params;
|
||||||
if (!['info', 'union'].includes(tab)) return res.status(400).json({ message: 'Некорректная вкладка' });
|
if (!['info', 'union', 'events', 'help', 'housing'].includes(tab)) return res.status(400).json({ message: 'Некорректная вкладка' });
|
||||||
try {
|
try {
|
||||||
await db.query('DELETE FROM content_blocks WHERE id = ? AND tab = ?', [id, tab]);
|
await db.query('DELETE FROM content_blocks WHERE id = ? AND tab = ?', [id, tab]);
|
||||||
res.json({ message: 'Удалено' });
|
res.json({ message: 'Удалено' });
|
||||||
@@ -96,5 +96,51 @@ router.delete('/:tab/:id', requireModerator, async (req, res) => {
|
|||||||
res.status(500).json({ message: 'Ошибка сервера' });
|
res.status(500).json({ message: 'Ошибка сервера' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
router.get('/section/:section', async (req, res) => {
|
||||||
|
const { section } = req.params;
|
||||||
|
if (!['events', 'help', 'housing'].includes(section)) {
|
||||||
|
return res.status(400).json({ message: 'Некорректный раздел' });
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const [rows] = await db.query(
|
||||||
|
'SELECT * FROM content_blocks WHERE tab = ? ORDER BY created_at ASC',
|
||||||
|
[section]
|
||||||
|
);
|
||||||
|
// Если есть блоки — возвращаем их, иначе пустой массив
|
||||||
|
res.json(rows);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
res.status(500).json({ message: 'Ошибка сервера' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Сохранить контент раздела (очищаем старые и вставляем новый)
|
||||||
|
router.post('/section/:section', requireModerator, async (req, res) => {
|
||||||
|
const { section } = req.params;
|
||||||
|
const { content } = req.body;
|
||||||
|
|
||||||
|
if (!['events', 'help', 'housing'].includes(section)) {
|
||||||
|
return res.status(400).json({ message: 'Некорректный раздел' });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Удаляем старые блоки для этого раздела
|
||||||
|
await db.query('DELETE FROM content_blocks WHERE tab = ?', [section]);
|
||||||
|
|
||||||
|
// Вставляем новый блок
|
||||||
|
const [result] = await db.query(
|
||||||
|
'INSERT INTO content_blocks (tab, type, content, created_by, created_at, updated_at) VALUES (?, ?, ?, ?, NOW(), NOW())',
|
||||||
|
[section, 'text', content, req.session.userId]
|
||||||
|
);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
id: result.insertId,
|
||||||
|
message: 'Раздел сохранён'
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
res.status(500).json({ message: 'Ошибка сервера' });
|
||||||
|
}
|
||||||
|
});
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
@@ -30,7 +30,7 @@ const sessionConfig = require('./config/session');
|
|||||||
app.use(helmet()); // Добавление helmet для защиты
|
app.use(helmet()); // Добавление helmet для защиты
|
||||||
app.use(cors(corsConfig));
|
app.use(cors(corsConfig));
|
||||||
app.use(morgan('dev'));
|
app.use(morgan('dev'));
|
||||||
app.use(express.json());
|
app.use(express.json({ limit: '100mb' }));
|
||||||
app.use(session(sessionConfig(session)));
|
app.use(session(sessionConfig(session)));
|
||||||
// Роуты
|
// Роуты
|
||||||
app.use('/api/auth', authRouter);
|
app.use('/api/auth', authRouter);
|
||||||
|
|||||||
Generated
+42
@@ -12,6 +12,8 @@
|
|||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^17.2.1",
|
"dotenv": "^17.2.1",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
|
"express-rate-limit": "^8.6.2",
|
||||||
|
"helmet": "^8.3.0",
|
||||||
"mammoth": "^1.9.1",
|
"mammoth": "^1.9.1",
|
||||||
"mysql2": "^3.14.2",
|
"mysql2": "^3.14.2",
|
||||||
"node-cron": "^4.2.1",
|
"node-cron": "^4.2.1",
|
||||||
@@ -598,6 +600,25 @@
|
|||||||
"url": "https://opencollective.com/express"
|
"url": "https://opencollective.com/express"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/express-rate-limit": {
|
||||||
|
"version": "8.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.6.2.tgz",
|
||||||
|
"integrity": "sha512-YH4ru+eOJxQABscKFfRCy9R7x9QFGdezclVMwwgFFndzS2Xnm0uo6B0ABZsLhcpeptGv2qvuJVWlQr9gQZoC3A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "^4.4.3",
|
||||||
|
"ip-address": "^10.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 16"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/express-rate-limit"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"express": ">= 4.11"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fecha": {
|
"node_modules/fecha": {
|
||||||
"version": "4.2.3",
|
"version": "4.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz",
|
||||||
@@ -791,6 +812,18 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/helmet": {
|
||||||
|
"version": "8.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/helmet/-/helmet-8.3.0.tgz",
|
||||||
|
"integrity": "sha512-Qgpiaws3Sm30Av8Eah6sjMCZZwjlBu+E68rhpCWBshY1lb09HtLwj5GviX0OyQIn+ulUS0iX0AxN5n3tLZzz1w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/EvanHahn"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/http-errors": {
|
"node_modules/http-errors": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
|
||||||
@@ -846,6 +879,15 @@
|
|||||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/ip-address": {
|
||||||
|
"version": "10.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.5.0.tgz",
|
||||||
|
"integrity": "sha512-R5SnVLJmgYYvf2F2ZgwSBnelz5G4q5AxIC277GDfUaNbrZKNANcBC7RHqYYePlszf4kBolVkJauG0ZjHHFh55g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ipaddr.js": {
|
"node_modules/ipaddr.js": {
|
||||||
"version": "1.9.1",
|
"version": "1.9.1",
|
||||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||||
|
|||||||
@@ -15,6 +15,8 @@
|
|||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^17.2.1",
|
"dotenv": "^17.2.1",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
|
"express-rate-limit": "^8.6.2",
|
||||||
|
"helmet": "^8.3.0",
|
||||||
"mammoth": "^1.9.1",
|
"mammoth": "^1.9.1",
|
||||||
"mysql2": "^3.14.2",
|
"mysql2": "^3.14.2",
|
||||||
"node-cron": "^4.2.1",
|
"node-cron": "^4.2.1",
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ async function readJson(filePath) {
|
|||||||
/**
|
/**
|
||||||
* Получение mock-данных сотрудника из config.json
|
* Получение mock-данных сотрудника из config.json
|
||||||
*/
|
*/
|
||||||
async function getMockEmployee(searchParam, type = 'card') {
|
async function getMockEmployee(searchParam) {
|
||||||
try {
|
try {
|
||||||
const configPath = path.join(__dirname, '../../config.json');
|
const configPath = path.join(__dirname, '../../config.json');
|
||||||
const config = await readJson(configPath);
|
const config = await readJson(configPath);
|
||||||
@@ -31,14 +31,20 @@ async function getMockEmployee(searchParam, type = 'card') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const mockEmployees = config.mockData.employees || [];
|
const mockEmployees = config.mockData.employees || [];
|
||||||
|
const paramStr = String(searchParam);
|
||||||
|
|
||||||
if (type === 'card') {
|
// Сначала пробуем найти по cardNumber (как строка, без приведения)
|
||||||
return mockEmployees.find(e => String(e.cardNumber) === String(searchParam)) || null;
|
let employee = mockEmployees.find(e => String(e.cardNumber) === paramStr);
|
||||||
} else if (type === 'id') {
|
|
||||||
return mockEmployees.find(e => e.id === parseInt(searchParam)) || null;
|
// Если не нашли — пробуем по id (как число)
|
||||||
|
if (!employee) {
|
||||||
|
const paramNum = parseInt(paramStr);
|
||||||
|
if (!isNaN(paramNum)) {
|
||||||
|
employee = mockEmployees.find(e => e.id === paramNum);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return employee || null;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error('[MOCK ERROR]', { error: err.message });
|
logger.error('[MOCK ERROR]', { error: err.message });
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
+34
-3
@@ -6,7 +6,13 @@ const { connectDB } = require('./config/db');
|
|||||||
const { logger } = require('./config/logger');
|
const { logger } = require('./config/logger');
|
||||||
const handbookRoutes = require('./routes/handbook');
|
const handbookRoutes = require('./routes/handbook');
|
||||||
const employeeRoutes = require('./routes/employee');
|
const employeeRoutes = require('./routes/employee');
|
||||||
|
const helmet = require('helmet');
|
||||||
|
const rateLimit = require('express-rate-limit');
|
||||||
require('dotenv').config({ path: path.join(__dirname, '../config.env') });
|
require('dotenv').config({ path: path.join(__dirname, '../config.env') });
|
||||||
|
const limiter = rateLimit({
|
||||||
|
windowMs: 15 * 60 * 1000,
|
||||||
|
max: 100
|
||||||
|
});
|
||||||
|
|
||||||
async function readJson(filePath) {
|
async function readJson(filePath) {
|
||||||
const data = await fs.readFile(filePath, 'utf8');
|
const data = await fs.readFile(filePath, 'utf8');
|
||||||
@@ -58,11 +64,20 @@ async function connectToNetworkShare(config) {
|
|||||||
const app = express();
|
const app = express();
|
||||||
app.use(cors());
|
app.use(cors());
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
if (req.method === 'POST' || req.method === 'PUT') {
|
||||||
|
if (!req.is('application/json')) {
|
||||||
|
return res.status(415).json({ message: 'Only JSON allowed' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// 📍 Маршруты API
|
// 📍 Маршруты API
|
||||||
// ============================================
|
// ============================================
|
||||||
|
app.use(helmet());
|
||||||
|
app.use('/api/', limiter);
|
||||||
app.use('/api/handbook', handbookRoutes);
|
app.use('/api/handbook', handbookRoutes);
|
||||||
app.use('/api/employee', employeeRoutes);
|
app.use('/api/employee', employeeRoutes);
|
||||||
|
|
||||||
@@ -320,7 +335,12 @@ app.get('/api/db-status', async (req, res) => {
|
|||||||
res.status(500).json({ status: 'error', message: 'DB connection failed' });
|
res.status(500).json({ status: 'error', message: 'DB connection failed' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
app.get('/api/time', (req, res) => {
|
||||||
|
res.json({
|
||||||
|
time: new Date().toISOString(),
|
||||||
|
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||||
|
});
|
||||||
|
});
|
||||||
async function startServer() {
|
async function startServer() {
|
||||||
try {
|
try {
|
||||||
const configPath = path.join(__dirname, '../config.json');
|
const configPath = path.join(__dirname, '../config.json');
|
||||||
@@ -328,6 +348,14 @@ async function startServer() {
|
|||||||
const { host, port } = config.server;
|
const { host, port } = config.server;
|
||||||
|
|
||||||
await connectDB();
|
await connectDB();
|
||||||
|
app.get('/api/time', (req, res) => {
|
||||||
|
const now = new Date();
|
||||||
|
logger.info(`[TIME] Запрос времени от ${req.ip || req.connection.remoteAddress}`);
|
||||||
|
res.json({
|
||||||
|
time: now.toISOString(),
|
||||||
|
timestamp: now.getTime()
|
||||||
|
});
|
||||||
|
});
|
||||||
app.listen(port, host, () => {
|
app.listen(port, host, () => {
|
||||||
logger.info(`Server is running on http://${host}:${port}`);
|
logger.info(`Server is running on http://${host}:${port}`);
|
||||||
});
|
});
|
||||||
@@ -339,5 +367,8 @@ async function startServer() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
app.use((err, req, res, next) => {
|
||||||
|
logger.error('Unhandled error', { error: err.message, stack: err.stack });
|
||||||
|
res.status(500).json({ message: 'Внутренняя ошибка сервера' });
|
||||||
|
});
|
||||||
startServer();
|
startServer();
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
VITE_NTP_SERVER=http://192.168.139.249:3001/api/time
|
||||||
Generated
+22
-77
@@ -13,11 +13,12 @@
|
|||||||
"@testing-library/user-event": "^13.5.0",
|
"@testing-library/user-event": "^13.5.0",
|
||||||
"@types/react": "^19.2.0",
|
"@types/react": "^19.2.0",
|
||||||
"@types/react-dom": "^19.2.0",
|
"@types/react-dom": "^19.2.0",
|
||||||
"axios": "^1.6.0",
|
"axios": "^1.19.0",
|
||||||
"date-fns": "^2.30.0",
|
"date-fns": "^2.30.0",
|
||||||
|
"dompurify": "^3.4.13",
|
||||||
"dotenv": "^17.4.2",
|
"dotenv": "^17.4.2",
|
||||||
"electron-is-dev": "^2.0.0",
|
"electron-is-dev": "^2.0.0",
|
||||||
"framer-motion": "^13.0.0",
|
"framer-motion": "^13.1.0",
|
||||||
"pdfjs-dist": "^3.11.174",
|
"pdfjs-dist": "^3.11.174",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
@@ -885,9 +886,6 @@
|
|||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1137,9 +1135,6 @@
|
|||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1157,9 +1152,6 @@
|
|||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1177,9 +1169,6 @@
|
|||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1197,9 +1186,6 @@
|
|||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1217,9 +1203,6 @@
|
|||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1237,9 +1220,6 @@
|
|||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1399,9 +1379,6 @@
|
|||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1416,9 +1393,6 @@
|
|||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1433,9 +1407,6 @@
|
|||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1450,9 +1421,6 @@
|
|||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1467,9 +1435,6 @@
|
|||||||
"loong64"
|
"loong64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1484,9 +1449,6 @@
|
|||||||
"loong64"
|
"loong64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1501,9 +1463,6 @@
|
|||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1518,9 +1477,6 @@
|
|||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1535,9 +1491,6 @@
|
|||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1552,9 +1505,6 @@
|
|||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1569,9 +1519,6 @@
|
|||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1586,9 +1533,6 @@
|
|||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1603,9 +1547,6 @@
|
|||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2040,6 +1981,13 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/trusted-types": {
|
||||||
|
"version": "2.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||||
|
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"node_modules/@types/yargs": {
|
"node_modules/@types/yargs": {
|
||||||
"version": "17.0.35",
|
"version": "17.0.35",
|
||||||
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz",
|
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz",
|
||||||
@@ -3737,6 +3685,15 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true
|
"peer": true
|
||||||
},
|
},
|
||||||
|
"node_modules/dompurify": {
|
||||||
|
"version": "3.4.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.13.tgz",
|
||||||
|
"integrity": "sha512-2vmYIoqjze2d+kakP8S/nS5shfsl587kzwEjcGlTdiksUVgFHnFCsLYDVj/JNqJVOQZGSYBTmuycv0PodwmnMQ==",
|
||||||
|
"license": "(MPL-2.0 OR Apache-2.0)",
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@types/trusted-types": "^2.0.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/dotenv": {
|
"node_modules/dotenv": {
|
||||||
"version": "17.4.2",
|
"version": "17.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz",
|
||||||
@@ -4656,9 +4613,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/framer-motion": {
|
"node_modules/framer-motion": {
|
||||||
"version": "13.0.0",
|
"version": "13.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-13.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-13.1.0.tgz",
|
||||||
"integrity": "sha512-nQGZXlsiigN48nzvE7AL1GLekql+etcphp8v+PQ0X04oxO20yVmV9rU9XQ25c136RdeIYoaWlKT9oAwvJza3mg==",
|
"integrity": "sha512-QSZrF0Id3QGuHJ+OL+9PSY9pk86C8ERFalwAGSchzTm65+ZoGH/RM26lmEARLljcHj2lqhv0jZOOks+EI3COOw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"motion-dom": "^13.0.0",
|
"motion-dom": "^13.0.0",
|
||||||
@@ -5783,9 +5740,6 @@
|
|||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -5807,9 +5761,6 @@
|
|||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -5831,9 +5782,6 @@
|
|||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -5855,9 +5803,6 @@
|
|||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
|
|||||||
@@ -9,11 +9,12 @@
|
|||||||
"@testing-library/user-event": "^13.5.0",
|
"@testing-library/user-event": "^13.5.0",
|
||||||
"@types/react": "^19.2.0",
|
"@types/react": "^19.2.0",
|
||||||
"@types/react-dom": "^19.2.0",
|
"@types/react-dom": "^19.2.0",
|
||||||
"axios": "^1.6.0",
|
"axios": "^1.19.0",
|
||||||
"date-fns": "^2.30.0",
|
"date-fns": "^2.30.0",
|
||||||
|
"dompurify": "^3.4.13",
|
||||||
"dotenv": "^17.4.2",
|
"dotenv": "^17.4.2",
|
||||||
"electron-is-dev": "^2.0.0",
|
"electron-is-dev": "^2.0.0",
|
||||||
"framer-motion": "^13.0.0",
|
"framer-motion": "^13.1.0",
|
||||||
"pdfjs-dist": "^3.11.174",
|
"pdfjs-dist": "^3.11.174",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useState, useEffect, useRef } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
|
const NTP_URL = import.meta.env.VITE_NTP_SERVER || 'http://192.168.139.249:3001/api/time';
|
||||||
interface CurrencyRate {
|
interface CurrencyRate {
|
||||||
currency: string;
|
currency: string;
|
||||||
rate: number;
|
rate: number;
|
||||||
@@ -30,7 +30,6 @@ const BottomBar: React.FC = () => {
|
|||||||
|
|
||||||
const [isOnline, setIsOnline] = useState(navigator.onLine);
|
const [isOnline, setIsOnline] = useState(navigator.onLine);
|
||||||
const [isPaused, setIsPaused] = useState(false);
|
const [isPaused, setIsPaused] = useState(false);
|
||||||
const [currentTime, setCurrentTime] = useState(new Date()); // ✅ Добавлено
|
|
||||||
const scrollRef = useRef<HTMLDivElement>(null);
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const getAllItems = () => {
|
const getAllItems = () => {
|
||||||
@@ -58,14 +57,57 @@ const BottomBar: React.FC = () => {
|
|||||||
const items = getAllItems();
|
const items = getAllItems();
|
||||||
const doubledItems = [...items, ...items, ...items];
|
const doubledItems = [...items, ...items, ...items];
|
||||||
|
|
||||||
// ✅ Обновление времени каждую секунду
|
const NTP_URL = 'http://localhost:3005/api/time'; // или твой URL
|
||||||
|
|
||||||
|
const useServerTime = (interval: number = 1000) => {
|
||||||
|
const [time, setTime] = useState(() => new Date());
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const interval = setInterval(() => {
|
let timer: NodeJS.Timeout;
|
||||||
setCurrentTime(new Date());
|
let diff = 0;
|
||||||
}, 1000);
|
|
||||||
|
const fetchTime = async () => {
|
||||||
|
try {
|
||||||
|
const res = await fetch(NTP_URL);
|
||||||
|
if (!res.ok) throw new Error('Server time unavailable');
|
||||||
|
const data = await res.json();
|
||||||
|
const serverTime = new Date(data.time);
|
||||||
|
|
||||||
|
// Вычисляем разницу между серверным и локальным временем
|
||||||
|
diff = serverTime.getTime() - Date.now();
|
||||||
|
|
||||||
|
// Устанавливаем время
|
||||||
|
setTime(new Date(Date.now() + diff));
|
||||||
|
|
||||||
|
// Запускаем локальный таймер
|
||||||
|
if (timer) clearInterval(timer);
|
||||||
|
timer = setInterval(() => {
|
||||||
|
setTime(new Date(Date.now() + diff));
|
||||||
|
}, interval);
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('Server time unavailable, using local time:', err);
|
||||||
|
// Если сервер недоступен — используем локальное время
|
||||||
|
setTime(new Date());
|
||||||
|
if (timer) clearInterval(timer);
|
||||||
|
timer = setInterval(() => {
|
||||||
|
setTime(new Date());
|
||||||
|
}, interval);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Первый запрос
|
||||||
|
fetchTime();
|
||||||
|
|
||||||
|
// Очистка при размонтировании
|
||||||
|
return () => {
|
||||||
|
if (timer) clearInterval(timer);
|
||||||
|
};
|
||||||
|
}, [interval]);
|
||||||
|
|
||||||
|
return time;
|
||||||
|
};
|
||||||
|
const currentTime = useServerTime(1000);
|
||||||
|
|
||||||
return () => clearInterval(interval);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleOnline = () => setIsOnline(true);
|
const handleOnline = () => setIsOnline(true);
|
||||||
@@ -308,4 +350,5 @@ const BottomBar: React.FC = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export default BottomBar;
|
export default BottomBar;
|
||||||
@@ -1,18 +1,79 @@
|
|||||||
import React from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { FaHandsHelping, FaCalendarCheck, FaMoneyBillWave, FaHome } from 'react-icons/fa';
|
import { FaHandsHelping, FaCalendarCheck, FaMoneyBillWave, FaHome, FaTimes } from 'react-icons/fa';
|
||||||
|
import DOMPurify from 'dompurify';
|
||||||
|
|
||||||
interface TradeUnionProps {
|
interface TradeUnionProps {
|
||||||
onBack: () => void;
|
onBack: () => void;
|
||||||
employee: any;
|
employee: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Функция-обертка для рендеринга иконок
|
interface Block {
|
||||||
const renderIcon = (Icon: any, style?: React.CSSProperties) => {
|
id: number;
|
||||||
return React.createElement(Icon, { style });
|
tab: string;
|
||||||
};
|
content: string;
|
||||||
|
created_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sections = [
|
||||||
|
{ id: 'events', title: 'Мероприятия', description: 'Анонсы и запись на мероприятия', icon: FaCalendarCheck, color: '#e74c3c' },
|
||||||
|
{ id: 'help', title: 'Материальная помощь', description: 'Размеры и виды материальной помощи', icon: FaMoneyBillWave, color: '#f39c12' },
|
||||||
|
{ id: 'housing', title: 'Жилищные условия', description: 'Информация об очереди на улучшение жилищных условий', icon: FaHome, color: '#3498db' },
|
||||||
|
];
|
||||||
|
|
||||||
const TradeUnion: React.FC<TradeUnionProps> = ({ onBack, employee }) => {
|
const TradeUnion: React.FC<TradeUnionProps> = ({ onBack, employee }) => {
|
||||||
|
const [selectedSection, setSelectedSection] = useState<string | null>(null);
|
||||||
|
const [blocksMap, setBlocksMap] = useState<Record<string, Block[]>>({});
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchAllSections = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const results: Record<string, Block[]> = {};
|
||||||
|
for (const sec of sections) {
|
||||||
|
const response = await fetch(`http://localhost:3005/moders/content/section/${sec.id}`, {
|
||||||
|
credentials: 'include',
|
||||||
|
});
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
results[sec.id] = Array.isArray(data) ? data : [];
|
||||||
|
} else {
|
||||||
|
results[sec.id] = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setBlocksMap(results);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Ошибка загрузки разделов профсоюза:', err);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fetchAllSections();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const openModal = (sectionId: string) => {
|
||||||
|
setSelectedSection(sectionId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeModal = () => {
|
||||||
|
setSelectedSection(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectedData = selectedSection ? sections.find(s => s.id === selectedSection) : null;
|
||||||
|
const selectedBlocks = selectedSection ? blocksMap[selectedSection] || [] : [];
|
||||||
|
|
||||||
|
const formatDate = (dateStr: string) => {
|
||||||
|
const date = new Date(dateStr);
|
||||||
|
return date.toLocaleString('ru-RU', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: '2-digit',
|
||||||
|
year: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
@@ -63,17 +124,27 @@ const TradeUnion: React.FC<TradeUnionProps> = ({ onBack, employee }) => {
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: '12px',
|
gap: '12px',
|
||||||
}}>
|
}}>
|
||||||
{renderIcon(FaHandsHelping, { color: '#e74c3c', fontSize: '24px' })}
|
<FaHandsHelping style={{ color: '#e74c3c', fontSize: '24px' }} />
|
||||||
Профсоюз
|
Профсоюз
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
|
{loading ? (
|
||||||
|
<div style={{ padding: '40px', textAlign: 'center', color: '#636e72' }}>Загрузка...</div>
|
||||||
|
) : (
|
||||||
<div style={{
|
<div style={{
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))',
|
gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))',
|
||||||
gap: '20px',
|
gap: '20px',
|
||||||
}}>
|
}}>
|
||||||
|
{sections.map((section) => {
|
||||||
|
const Icon = section.icon;
|
||||||
|
const blocks = blocksMap[section.id] || [];
|
||||||
|
const hasContent = blocks.length > 0;
|
||||||
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
|
key={section.id}
|
||||||
whileHover={{ y: -5 }}
|
whileHover={{ y: -5 }}
|
||||||
|
onClick={() => openModal(section.id)}
|
||||||
style={{
|
style={{
|
||||||
background: 'white',
|
background: 'white',
|
||||||
borderRadius: '16px',
|
borderRadius: '16px',
|
||||||
@@ -81,80 +152,224 @@ const TradeUnion: React.FC<TradeUnionProps> = ({ onBack, employee }) => {
|
|||||||
boxShadow: '0 4px 20px rgba(0,0,0,0.06)',
|
boxShadow: '0 4px 20px rgba(0,0,0,0.06)',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
transition: 'all 0.3s ease',
|
transition: 'all 0.3s ease',
|
||||||
border: '1px solid #f1f2f6',
|
border: `1px solid ${hasContent ? section.color : '#f1f2f6'}`,
|
||||||
|
position: 'relative',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ fontSize: '32px', color: '#e74c3c', marginBottom: '15px' }}>
|
{hasContent && (
|
||||||
{renderIcon(FaCalendarCheck)}
|
|
||||||
</div>
|
|
||||||
<h3 style={{ margin: '0 0 10px 0', color: '#2d3436', fontSize: '18px' }}>
|
|
||||||
Мероприятия
|
|
||||||
</h3>
|
|
||||||
<p style={{ color: '#636e72', fontSize: '14px', lineHeight: 1.6, margin: 0 }}>
|
|
||||||
Анонсы и запись на мероприятия
|
|
||||||
</p>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<motion.div
|
|
||||||
whileHover={{ y: -5 }}
|
|
||||||
style={{
|
|
||||||
background: 'white',
|
|
||||||
borderRadius: '16px',
|
|
||||||
padding: '25px',
|
|
||||||
boxShadow: '0 4px 20px rgba(0,0,0,0.06)',
|
|
||||||
cursor: 'pointer',
|
|
||||||
transition: 'all 0.3s ease',
|
|
||||||
border: '1px solid #f1f2f6',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div style={{ fontSize: '32px', color: '#f39c12', marginBottom: '15px' }}>
|
|
||||||
{renderIcon(FaMoneyBillWave)}
|
|
||||||
</div>
|
|
||||||
<h3 style={{ margin: '0 0 10px 0', color: '#2d3436', fontSize: '18px' }}>
|
|
||||||
Материальная помощь
|
|
||||||
</h3>
|
|
||||||
<p style={{ color: '#636e72', fontSize: '14px', lineHeight: 1.6, margin: 0 }}>
|
|
||||||
Размеры и виды материальной помощи
|
|
||||||
</p>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<motion.div
|
|
||||||
whileHover={{ y: -5 }}
|
|
||||||
style={{
|
|
||||||
background: 'white',
|
|
||||||
borderRadius: '16px',
|
|
||||||
padding: '25px',
|
|
||||||
boxShadow: '0 4px 20px rgba(0,0,0,0.06)',
|
|
||||||
cursor: 'pointer',
|
|
||||||
transition: 'all 0.3s ease',
|
|
||||||
border: '1px solid #f1f2f6',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div style={{ fontSize: '32px', color: '#3498db', marginBottom: '15px' }}>
|
|
||||||
{renderIcon(FaHome)}
|
|
||||||
</div>
|
|
||||||
<h3 style={{ margin: '0 0 10px 0', color: '#2d3436', fontSize: '18px' }}>
|
|
||||||
Жилищные условия
|
|
||||||
</h3>
|
|
||||||
<p style={{ color: '#636e72', fontSize: '14px', lineHeight: 1.6, margin: 0 }}>
|
|
||||||
Информация об очереди на улучшение жилищных условий
|
|
||||||
</p>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{
|
<div style={{
|
||||||
marginTop: '30px',
|
position: 'absolute',
|
||||||
padding: '20px',
|
top: '12px',
|
||||||
background: '#f8f9fa',
|
right: '12px',
|
||||||
|
background: section.color,
|
||||||
|
color: 'white',
|
||||||
|
fontSize: '10px',
|
||||||
|
padding: '2px 10px',
|
||||||
borderRadius: '12px',
|
borderRadius: '12px',
|
||||||
border: '1px solid #f1f2f6',
|
fontWeight: 600,
|
||||||
textAlign: 'center',
|
|
||||||
color: '#636e72',
|
|
||||||
}}>
|
}}>
|
||||||
<p style={{ margin: 0 }}>
|
{blocks.length} блок(ов)
|
||||||
ℹ️ Раздел в разработке. Функционал будет доступен в ближайшее время.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
<div style={{ fontSize: '32px', color: section.color, marginBottom: '15px' }}>
|
||||||
|
<Icon />
|
||||||
|
</div>
|
||||||
|
<h3 style={{ margin: '0 0 10px 0', color: '#2d3436', fontSize: '18px' }}>
|
||||||
|
{section.title}
|
||||||
|
</h3>
|
||||||
|
<p style={{ color: '#636e72', fontSize: '14px', lineHeight: 1.6, margin: 0 }}>
|
||||||
|
{section.description}
|
||||||
|
</p>
|
||||||
|
{!hasContent && (
|
||||||
|
<div style={{
|
||||||
|
marginTop: '12px',
|
||||||
|
fontSize: '12px',
|
||||||
|
color: '#b2bec3',
|
||||||
|
}}>
|
||||||
|
Информация отсутствует
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* ===== МОДАЛКА ===== */}
|
||||||
|
{selectedSection && selectedData && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'fixed',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
background: 'rgba(0,0,0,0.6)',
|
||||||
|
backdropFilter: 'blur(6px)',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
zIndex: 1000,
|
||||||
|
padding: '20px',
|
||||||
|
}}
|
||||||
|
onClick={(e) => {
|
||||||
|
if (e.target === e.currentTarget) closeModal();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
background: 'white',
|
||||||
|
borderRadius: '20px',
|
||||||
|
width: '70%',
|
||||||
|
height: '80%',
|
||||||
|
boxShadow: '0 30px 80px rgba(0,0,0,0.3)',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
overflow: 'hidden',
|
||||||
|
position: 'relative',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Шапка (заголовок + крестик) — фиксирована */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
padding: '20px 30px 10px 30px',
|
||||||
|
borderBottom: '1px solid #eee',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
flexShrink: 0,
|
||||||
|
background: 'white',
|
||||||
|
zIndex: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<h3 style={{
|
||||||
|
color: selectedData.color,
|
||||||
|
fontSize: '22px',
|
||||||
|
margin: 0,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '12px',
|
||||||
|
}}>
|
||||||
|
<selectedData.icon style={{ fontSize: '24px' }} />
|
||||||
|
{selectedData.title}
|
||||||
|
</h3>
|
||||||
|
<div style={{ flex: 1, display: 'flex', justifyContent: 'flex-end' }}>
|
||||||
|
<button
|
||||||
|
onClick={closeModal}
|
||||||
|
style={{
|
||||||
|
background: 'none',
|
||||||
|
border: 'none',
|
||||||
|
fontSize: '26px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
color: '#999',
|
||||||
|
padding: '0 8px',
|
||||||
|
transition: 'color 0.2s',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'right',
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => e.currentTarget.style.color = '#333'}
|
||||||
|
onMouseLeave={(e) => e.currentTarget.style.color = '#999'}
|
||||||
|
>
|
||||||
|
<FaTimes />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Тело с прокруткой */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
overflowY: 'auto',
|
||||||
|
padding: '20px 30px 30px 30px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{selectedBlocks.length === 0 ? (
|
||||||
|
<div style={{
|
||||||
|
padding: '40px',
|
||||||
|
textAlign: 'center',
|
||||||
|
color: '#b2bec3',
|
||||||
|
fontSize: '16px',
|
||||||
|
}}>
|
||||||
|
Информация отсутствует
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: '16px',
|
||||||
|
}}>
|
||||||
|
{selectedBlocks.map((block) => (
|
||||||
|
<div
|
||||||
|
key={block.id}
|
||||||
|
style={{
|
||||||
|
border: '1px solid #e8e8e8',
|
||||||
|
borderRadius: '12px',
|
||||||
|
padding: '18px 20px',
|
||||||
|
background: '#fafafa',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: '15px',
|
||||||
|
lineHeight: 1.7,
|
||||||
|
color: '#2d3436',
|
||||||
|
}}
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: DOMPurify.sanitize(block.content)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div style={{
|
||||||
|
marginTop: '14px',
|
||||||
|
paddingTop: '12px',
|
||||||
|
borderTop: '1px solid #eee',
|
||||||
|
fontSize: '12px',
|
||||||
|
color: '#999',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
}}>
|
||||||
|
<span>🕒 {formatDate(block.created_at)}</span>
|
||||||
|
<span style={{ fontSize: '11px', color: '#bbb' }}>ID: {block.id}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Нижняя кнопка закрытия */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
padding: '12px 30px 20px 30px',
|
||||||
|
borderTop: '1px solid #eee',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
flexShrink: 0,
|
||||||
|
background: 'white',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
onClick={closeModal}
|
||||||
|
style={{
|
||||||
|
padding: '10px 40px',
|
||||||
|
background: '#6c757d',
|
||||||
|
color: 'white',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '10px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: '14px',
|
||||||
|
fontWeight: 500,
|
||||||
|
transition: 'background 0.2s',
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => e.currentTarget.style.background = '#5a6268'}
|
||||||
|
onMouseLeave={(e) => e.currentTarget.style.background = '#6c757d'}
|
||||||
|
>
|
||||||
|
Закрыть
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user