diff --git a/.gitea/workflows/docker-publish-tabler.yml b/.gitea/workflows/docker-publish-tabler.yml
new file mode 100644
index 0000000..3482dc8
--- /dev/null
+++ b/.gitea/workflows/docker-publish-tabler.yml
@@ -0,0 +1,37 @@
+name: Publish Tabler Docker image
+
+on:
+ push:
+ branches:
+ - tabler
+
+jobs:
+ build-and-push-tabler:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Log in to Gitea Registry
+ uses: docker/login-action@v3
+ with:
+ registry: git.shts.su
+ username: ${{ gitea.actor }}
+ password: ${{ secrets.ACTIONS_PAT }}
+
+ - name: Build and push Tabler Docker image
+ uses: docker/build-push-action@v5
+ with:
+ context: .
+ file: ./Dockerfile
+ push: true
+ platforms: linux/amd64
+ tags: |
+ git.shts.su/${{ gitea.repository }}:tabler
+ git.shts.su/${{ gitea.repository }}:tabler-${{ gitea.sha }}
+ labels: |
+ org.opencontainers.image.title=S3 Lists Manager with Tabler UI
+ org.opencontainers.image.description=Modern S3 Lists Manager with Tabler UI design
+ org.opencontainers.image.version=tabler
+ org.opencontainers.image.revision=${{ gitea.sha }}
+ org.opencontainers.image.created=${{ gitea.event.head_commit.timestamp }}
\ No newline at end of file
diff --git a/.gitea/workflows/docker-publish.yml b/.gitea/workflows/docker-publish.yml
index 6cf1dfe..c9074e7 100644
--- a/.gitea/workflows/docker-publish.yml
+++ b/.gitea/workflows/docker-publish.yml
@@ -28,4 +28,10 @@ jobs:
platforms: linux/amd64
tags: |
git.shts.su/${{ gitea.repository }}:latest
- git.shts.su/${{ gitea.repository }}:${{ gitea.sha }}
\ No newline at end of file
+ git.shts.su/${{ gitea.repository }}:${{ gitea.sha }}
+ labels: |
+ org.opencontainers.image.title=S3 Lists Manager
+ org.opencontainers.image.description=S3 Lists Manager for domains and AS management
+ org.opencontainers.image.version=latest
+ org.opencontainers.image.revision=${{ gitea.sha }}
+ org.opencontainers.image.created=${{ gitea.event.head_commit.timestamp }}
\ No newline at end of file
diff --git a/DOCKER.md b/DOCKER.md
new file mode 100644
index 0000000..3b8bff9
--- /dev/null
+++ b/DOCKER.md
@@ -0,0 +1,208 @@
+# Docker Images
+
+Этот проект предоставляет Docker образы для S3 Lists Manager с двумя различными версиями интерфейса.
+
+## 🏷️ Доступные теги
+
+### Основная версия (main branch)
+- `git.shts.su/[repository]:latest` - Стабильная версия с базовым интерфейсом
+- `git.shts.su/[repository]:[commit-sha]` - Версия с конкретным коммитом
+
+### Tabler версия (tabler branch)
+- `git.shts.su/[repository]:tabler` - Версия с современным Tabler UI
+- `git.shts.su/[repository]:tabler-[commit-sha]` - Tabler версия с конкретным коммитом
+
+## 🚀 Запуск
+
+### Основная версия
+```bash
+docker run -d \
+ --name s3-lists-manager \
+ -p 3001:3001 \
+ git.shts.su/[repository]:latest
+```
+
+### Tabler версия
+```bash
+docker run -d \
+ --name s3-lists-manager-tabler \
+ -p 3001:3001 \
+ git.shts.su/[repository]:tabler
+```
+
+## 🔄 Обновление
+
+### Основная версия
+```bash
+docker pull git.shts.su/[repository]:latest
+docker stop s3-lists-manager
+docker rm s3-lists-manager
+docker run -d \
+ --name s3-lists-manager \
+ -p 3001:3001 \
+ git.shts.su/[repository]:latest
+```
+
+### Tabler версия
+```bash
+docker pull git.shts.su/[repository]:tabler
+docker stop s3-lists-manager-tabler
+docker rm s3-lists-manager-tabler
+docker run -d \
+ --name s3-lists-manager-tabler \
+ -p 3001:3001 \
+ git.shts.su/[repository]:tabler
+```
+
+## 🐳 Docker Compose
+
+Создайте файл `docker-compose.yml`:
+
+```yaml
+version: '3.8'
+
+services:
+ s3-lists-manager:
+ image: git.shts.su/[repository]:latest
+ container_name: s3-lists-manager
+ ports:
+ - "3001:3001"
+ restart: unless-stopped
+ environment:
+ - NODE_ENV=production
+
+ s3-lists-manager-tabler:
+ image: git.shts.su/[repository]:tabler
+ container_name: s3-lists-manager-tabler
+ ports:
+ - "3002:3001"
+ restart: unless-stopped
+ environment:
+ - NODE_ENV=production
+```
+
+Запуск:
+```bash
+docker-compose up -d
+```
+
+## 🔧 Переменные окружения
+
+| Переменная | Описание | По умолчанию |
+|------------|----------|--------------|
+| `NODE_ENV` | Окружение Node.js | `production` |
+| `PORT` | Порт для запуска сервера | `3001` |
+
+## 📊 Мониторинг
+
+### Просмотр логов
+```bash
+# Основная версия
+docker logs s3-lists-manager
+
+# Tabler версия
+docker logs s3-lists-manager-tabler
+```
+
+### Статистика контейнеров
+```bash
+docker stats s3-lists-manager s3-lists-manager-tabler
+```
+
+## 🔍 Отладка
+
+### Вход в контейнер
+```bash
+# Основная версия
+docker exec -it s3-lists-manager /bin/bash
+
+# Tabler версия
+docker exec -it s3-lists-manager-tabler /bin/bash
+```
+
+### Проверка процессов
+```bash
+docker exec s3-lists-manager ps aux
+```
+
+## 🏗️ Сборка локально
+
+### Основная версия
+```bash
+git checkout main
+docker build -t s3-lists-manager:local .
+```
+
+### Tabler версия
+```bash
+git checkout tabler
+docker build -t s3-lists-manager-tabler:local .
+```
+
+## 📝 CI/CD
+
+### Автоматическая сборка
+
+- **Ветка `main`**: Автоматически создает образ с тегом `latest`
+- **Ветка `tabler`**: Автоматически создает образ с тегом `tabler`
+
+### Workflow файлы
+
+- `.gitea/workflows/docker-publish.yml` - Сборка основной версии
+- `.gitea/workflows/docker-publish-tabler.yml` - Сборка Tabler версии
+
+## 🔒 Безопасность
+
+### Проверка образа
+```bash
+docker scan git.shts.su/[repository]:latest
+docker scan git.shts.su/[repository]:tabler
+```
+
+### Запуск в изолированной сети
+```bash
+docker network create s3-lists-network
+docker run -d \
+ --name s3-lists-manager \
+ --network s3-lists-network \
+ -p 3001:3001 \
+ git.shts.su/[repository]:latest
+```
+
+## 📈 Производительность
+
+### Ограничение ресурсов
+```bash
+docker run -d \
+ --name s3-lists-manager \
+ --memory=512m \
+ --cpus=0.5 \
+ -p 3001:3001 \
+ git.shts.su/[repository]:latest
+```
+
+### Мониторинг ресурсов
+```bash
+docker stats --no-stream s3-lists-manager
+```
+
+## 🆘 Устранение неполадок
+
+### Проверка статуса контейнера
+```bash
+docker ps -a | grep s3-lists-manager
+```
+
+### Перезапуск контейнера
+```bash
+docker restart s3-lists-manager
+```
+
+### Очистка неиспользуемых образов
+```bash
+docker image prune -f
+```
+
+---
+
+**Примечание**: Замените `[repository]` на актуальное имя вашего репозитория в Gitea.
\ No newline at end of file
diff --git a/README.md b/README.md
index 7971461..8eb3510 100644
--- a/README.md
+++ b/README.md
@@ -12,6 +12,7 @@
- **Сохранение в S3:** Все изменения сохраняются непосредственно в файлах в бакете Yandex Cloud.
- **Docker-контейнеризация:** Готовый `Dockerfile` для сборки и запуска приложения в изолированном окружении.
- **CI/CD с Gitea Actions:** Автоматическая сборка и публикация Docker-образа в Gitea Registry при пуше в `main`.
+- **Два варианта интерфейса:** Базовая версия и современная версия с Tabler UI.
## 🛠️ Технологический стек
@@ -19,6 +20,7 @@
|--------------|-----------------------------------------------------------------------------------------------------------|
| **Фронтенд** | [**React**](https://reactjs.org/) + [**Vite**](https://vitejs.dev/) |
| | [**React-Bootstrap**](https://react-bootstrap.github.io/) (UI-компоненты) |
+| | [**Tabler Icons**](https://tabler-icons.io/) (иконки для Tabler версии) |
| | [**Axios**](https://axios-http.com/) (HTTP-клиент) |
| **Бэкенд** | [**Node.js**](https://nodejs.org/) + [**Express**](https://expressjs.com/) |
| | [**AWS SDK for JS**](https://aws.amazon.com/sdk-for-javascript/) (для работы с Yandex Cloud S3) |
@@ -103,9 +105,41 @@ graph TD
## 🐳 Docker
-Приложение полностью готово к запуску в Docker.
+Приложение полностью готово к запуску в Docker с двумя вариантами интерфейса.
-### Сборка образа
+### Доступные образы
+
+#### Основная версия (main branch)
+```bash
+git.shts.su/[repository]:latest
+```
+
+#### Tabler версия (tabler branch)
+```bash
+git.shts.su/[repository]:tabler
+```
+
+### Быстрый запуск
+
+#### Основная версия
+```bash
+docker run -d \
+ --name s3-lists-manager \
+ -p 3001:3001 \
+ --env-file ./backend/.env \
+ git.shts.su/[repository]:latest
+```
+
+#### Tabler версия
+```bash
+docker run -d \
+ --name s3-lists-manager-tabler \
+ -p 3002:3001 \
+ --env-file ./backend/.env \
+ git.shts.su/[repository]:tabler
+```
+
+### Локальная сборка
Для сборки образа выполните команду в корневой директории проекта:
```bash
@@ -122,6 +156,40 @@ docker run --rm -p 3001:3001 --env-file ./backend/.env s3-lists-manager
После этого приложение будет доступно по адресу `http://localhost:3001`.
+### Docker Compose
+
+Создайте файл `docker-compose.yml`:
+
+```yaml
+version: '3.8'
+
+services:
+ s3-lists-manager:
+ image: git.shts.su/[repository]:latest
+ container_name: s3-lists-manager
+ ports:
+ - "3001:3001"
+ restart: unless-stopped
+ env_file:
+ - ./backend/.env
+
+ s3-lists-manager-tabler:
+ image: git.shts.su/[repository]:tabler
+ container_name: s3-lists-manager-tabler
+ ports:
+ - "3002:3001"
+ restart: unless-stopped
+ env_file:
+ - ./backend/.env
+```
+
+Запуск:
+```bash
+docker-compose up -d
+```
+
+Подробная документация по Docker образам доступна в файле [DOCKER.md](DOCKER.md).
+
## ⚙️ API Endpoints
| Метод | Путь | Описание |
diff --git a/frontend/README.md b/frontend/README.md
index 7059a96..1fd780d 100644
--- a/frontend/README.md
+++ b/frontend/README.md
@@ -1,12 +1,175 @@
-# React + Vite
+# S3 Lists Manager - Frontend
-This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
+Современный веб-интерфейс для управления списками доменов и AS с использованием Tabler UI.
-Currently, two official plugins are available:
+## 🎨 Особенности
-- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh
-- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
+- **Современный UI/UX** - Полностью переработанный интерфейс в стиле Tabler
+- **Адаптивный дизайн** - Отлично работает на всех устройствах
+- **Иконки Tabler** - Красивые иконки для улучшения пользовательского опыта
+- **Интерактивные элементы** - Анимации, hover-эффекты, модальные окна
+- **Статистика** - Отображение количества элементов в реальном времени
+- **Поиск** - Быстрый поиск по спискам
+- **Массовые операции** - Возможность массового обновления шлюзов
+- **Подтверждения** - Модальные окна для подтверждения важных действий
-## Expanding the ESLint configuration
+## 🚀 Технологии
-If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.
+- **React 19** - Современная библиотека для создания пользовательских интерфейсов
+- **Tabler Icons** - Красивые иконки в стиле Tabler
+- **CSS Variables** - Современные CSS переменные для легкой кастомизации
+- **Responsive Design** - Адаптивный дизайн для всех устройств
+- **Vite** - Быстрый сборщик проекта
+
+## 📦 Установка
+
+1. Убедитесь, что у вас установлен Node.js (версия 16 или выше)
+
+2. Перейдите в папку frontend:
+```bash
+cd frontend
+```
+
+3. Установите зависимости:
+```bash
+npm install
+```
+
+4. Запустите проект в режиме разработки:
+```bash
+npm run dev
+```
+
+5. Откройте браузер и перейдите по адресу: `http://localhost:5173`
+
+## 🏗️ Сборка для продакшена
+
+```bash
+npm run build
+```
+
+Собранные файлы будут находиться в папке `dist/`.
+
+## 🎯 Использование
+
+### Управление доменами
+
+1. **Добавление домена**:
+ - Введите имя домена (например: `example.com`)
+ - Выберите шлюз (по умолчанию: `SharkFIN`)
+ - Нажмите "Добавить"
+
+2. **Редактирование**:
+ - Нажмите на иконку редактирования рядом с доменом
+ - Измените шлюз
+ - Нажмите "Сохранить" или "Отмена"
+
+3. **Удаление**:
+ - Нажмите на иконку удаления
+ - Подтвердите удаление в модальном окне
+
+### Управление AS
+
+Аналогично управлению доменами, но для AS номеров (например: `AS12345`).
+
+### Массовые операции
+
+1. Введите текущий шлюз в поле "Заменить с"
+2. Введите новый шлюз в поле "Заменить на"
+3. Нажмите "Выполнить замену"
+4. Подтвердите операцию
+
+### Сохранение изменений
+
+После внесения изменений обязательно нажмите "Сохранить в S3" для записи изменений в базу данных.
+
+## 🎨 Кастомизация
+
+### Цветовая схема
+
+Основные цвета можно изменить в CSS переменных в файле `src/App.css`:
+
+```css
+:root {
+ --tblr-primary: #206bc4; /* Основной цвет */
+ --tblr-success: #2fb344; /* Цвет успеха */
+ --tblr-warning: #f59f00; /* Цвет предупреждения */
+ --tblr-danger: #d63939; /* Цвет ошибки */
+}
+```
+
+### Шрифты
+
+Проект использует шрифт Inter. Для изменения шрифта обновите переменную:
+
+```css
+--tblr-font-family-sans-serif: "Ваш шрифт", -apple-system, BlinkMacSystemFont, ...;
+```
+
+## 📱 Адаптивность
+
+Интерфейс полностью адаптивен и оптимизирован для:
+
+- **Desktop** (1200px+) - Полный интерфейс с боковой панелью
+- **Tablet** (768px - 1199px) - Адаптированный интерфейс
+- **Mobile** (до 767px) - Мобильная версия с выпадающим меню
+
+## 🔧 Разработка
+
+### Структура проекта
+
+```
+src/
+├── App.jsx # Главный компонент приложения
+├── App.css # Стили Tabler
+├── DataManager.jsx # Компонент управления данными
+├── main.jsx # Точка входа
+└── index.css # Базовые стили
+```
+
+### Добавление новых функций
+
+1. Создайте новый компонент в папке `src/`
+2. Импортируйте необходимые иконки из `@tabler/icons-react`
+3. Используйте CSS классы Tabler для стилизации
+4. Добавьте адаптивность для мобильных устройств
+
+### Иконки
+
+Все иконки импортируются из `@tabler/icons-react`:
+
+```jsx
+import { IconPlus, IconEdit, IconTrash } from '@tabler/icons-react';
+
+// Использование
+
+```
+
+## 🐛 Отладка
+
+### Консоль разработчика
+
+Откройте консоль разработчика (F12) для просмотра ошибок и логов.
+
+### Проверка API
+
+Убедитесь, что бэкенд запущен и доступен по адресу `/api`.
+
+## 📄 Лицензия
+
+Этот проект использует лицензию MIT.
+
+## 🤝 Вклад в проект
+
+1. Форкните репозиторий
+2. Создайте ветку для новой функции
+3. Внесите изменения
+4. Создайте Pull Request
+
+## 📞 Поддержка
+
+Если у вас есть вопросы или проблемы, создайте Issue в репозитории.
+
+---
+
+**S3 Lists Manager** - Современный интерфейс для управления списками с использованием Tabler UI.
diff --git a/frontend/index.html b/frontend/index.html
index 0c589ec..08537b6 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -1,10 +1,18 @@
-
+
- Vite + React
+
+
+
+
+
+
+
+
+ S3 Lists Manager
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 534c99f..25a1e88 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -8,6 +8,7 @@
"name": "frontend",
"version": "0.0.0",
"dependencies": {
+ "@tabler/icons-react": "^3.34.0",
"axios": "^1.10.0",
"bootstrap": "^5.3.7",
"react": "^19.1.0",
@@ -641,6 +642,32 @@
"tslib": "^2.8.0"
}
},
+ "node_modules/@tabler/icons": {
+ "version": "3.34.0",
+ "resolved": "https://registry.npmjs.org/@tabler/icons/-/icons-3.34.0.tgz",
+ "integrity": "sha512-jtVqv0JC1WU2TTEBN32D9+R6mc1iEBuPwLnBsWaR02SIEciu9aq5806AWkCHuObhQ4ERhhXErLEK7Fs+tEZxiA==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/codecalm"
+ }
+ },
+ "node_modules/@tabler/icons-react": {
+ "version": "3.34.0",
+ "resolved": "https://registry.npmjs.org/@tabler/icons-react/-/icons-react-3.34.0.tgz",
+ "integrity": "sha512-OpEIR2iZsIXECtAIMbn1zfKfQ3zKJjXyIZlkgOGUL9UkMCFycEiF2Y8AVfEQsyre/3FnBdlWJvGr0NU47n2TbQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@tabler/icons": "3.34.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/codecalm"
+ },
+ "peerDependencies": {
+ "react": ">= 16"
+ }
+ },
"node_modules/@types/babel__core": {
"version": "7.20.5",
"dev": true,
diff --git a/frontend/package.json b/frontend/package.json
index 1a67cbf..8093a2c 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -10,6 +10,7 @@
"preview": "vite preview"
},
"dependencies": {
+ "@tabler/icons-react": "^3.34.0",
"axios": "^1.10.0",
"bootstrap": "^5.3.7",
"react": "^19.1.0",
diff --git a/frontend/src/App.css b/frontend/src/App.css
index b9d355d..2e8e033 100644
--- a/frontend/src/App.css
+++ b/frontend/src/App.css
@@ -40,3 +40,932 @@
.read-the-docs {
color: #888;
}
+
+/* Tabler CSS Variables */
+:root {
+ --tblr-primary: #206bc4;
+ --tblr-primary-rgb: 32, 107, 196;
+ --tblr-secondary: #6c757d;
+ --tblr-secondary-rgb: 108, 117, 125;
+ --tblr-success: #2fb344;
+ --tblr-success-rgb: 47, 179, 68;
+ --tblr-info: #467fd0;
+ --tblr-info-rgb: 70, 127, 208;
+ --tblr-warning: #f59f00;
+ --tblr-warning-rgb: 245, 159, 0;
+ --tblr-danger: #d63939;
+ --tblr-danger-rgb: 214, 57, 57;
+ --tblr-light: #f8f9fa;
+ --tblr-light-rgb: 248, 249, 250;
+ --tblr-dark: #1e293b;
+ --tblr-dark-rgb: 30, 41, 59;
+ --tblr-body-bg: #f5f6f7;
+ --tblr-body-color: #1e293b;
+ --tblr-border-color: #e9ecef;
+ --tblr-border-radius: 0.375rem;
+ --tblr-border-radius-sm: 0.25rem;
+ --tblr-border-radius-lg: 0.5rem;
+ --tblr-box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
+ --tblr-box-shadow-sm: 0 0.0625rem 0.125rem rgba(0, 0, 0, 0.075);
+ --tblr-box-shadow-lg: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
+ --tblr-font-family-sans-serif: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
+ --tblr-font-family-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
+ --tblr-font-size-base: 0.875rem;
+ --tblr-line-height-base: 1.5;
+ --tblr-heading-font-weight: 600;
+ --tblr-btn-font-weight: 500;
+ --tblr-btn-border-radius: 0.375rem;
+ --tblr-btn-padding-x: 0.75rem;
+ --tblr-btn-padding-y: 0.375rem;
+ --tblr-input-border-radius: 0.375rem;
+ --tblr-input-padding-x: 0.75rem;
+ --tblr-input-padding-y: 0.375rem;
+ --tblr-card-border-radius: 0.5rem;
+ --tblr-card-border-width: 0;
+ --tblr-card-box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
+ --tblr-navbar-height: 3.5rem;
+ --tblr-sidebar-width: 16rem;
+ --tblr-sidebar-width-md: 12rem;
+ --tblr-sidebar-width-sm: 4rem;
+}
+
+/* Base Styles */
+* {
+ box-sizing: border-box;
+}
+
+body {
+ font-family: var(--tblr-font-family-sans-serif);
+ font-size: var(--tblr-font-size-base);
+ line-height: var(--tblr-line-height-base);
+ color: var(--tblr-body-color);
+ background-color: var(--tblr-body-bg);
+ margin: 0;
+ padding: 0;
+}
+
+/* Page Layout */
+.page {
+ min-height: 100vh;
+ display: flex;
+ flex-direction: column;
+}
+
+.page-wrapper {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+}
+
+.page-body {
+ flex: 1;
+ padding: 1.5rem 0;
+}
+
+.page-header {
+ padding: 1.5rem 0;
+ border-bottom: 1px solid var(--tblr-border-color);
+ margin-bottom: 1.5rem;
+}
+
+.page-title {
+ font-size: 1.5rem;
+ font-weight: var(--tblr-heading-font-weight);
+ margin: 0;
+ color: var(--tblr-dark);
+}
+
+/* Navbar */
+.navbar {
+ background-color: #fff;
+ border-bottom: 1px solid var(--tblr-border-color);
+ box-shadow: var(--tblr-box-shadow-sm);
+ height: var(--tblr-navbar-height);
+ padding: 0;
+}
+
+.navbar-brand {
+ font-weight: var(--tblr-heading-font-weight);
+ font-size: 1.25rem;
+ color: var(--tblr-dark);
+ text-decoration: none;
+ display: flex;
+ align-items: center;
+}
+
+.navbar-brand:hover {
+ color: var(--tblr-primary);
+}
+
+.navbar-brand-icon {
+ width: 1.5rem;
+ height: 1.5rem;
+}
+
+.navbar-toggler {
+ border: none;
+ background: none;
+ padding: 0.5rem;
+ color: var(--tblr-secondary);
+ cursor: pointer;
+}
+
+.navbar-toggler:hover {
+ color: var(--tblr-dark);
+}
+
+.navbar-nav {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ display: flex;
+ align-items: center;
+}
+
+.nav-item {
+ margin: 0;
+}
+
+.nav-link {
+ display: flex;
+ align-items: center;
+ padding: 0.75rem 1rem;
+ color: var(--tblr-secondary);
+ text-decoration: none;
+ border-radius: var(--tblr-border-radius);
+ transition: all 0.15s ease-in-out;
+}
+
+.nav-link:hover {
+ color: var(--tblr-dark);
+ background-color: var(--tblr-light);
+}
+
+.nav-link.active {
+ color: var(--tblr-primary);
+ background-color: rgba(var(--tblr-primary-rgb), 0.1);
+}
+
+.nav-link-icon {
+ width: 1.25rem;
+ height: 1.25rem;
+ margin-right: 0.5rem;
+}
+
+.nav-link-title {
+ font-weight: 500;
+}
+
+/* Avatar */
+.avatar {
+ display: inline-block;
+ width: 2rem;
+ height: 2rem;
+ border-radius: 50%;
+ background-size: cover;
+ background-position: center;
+ background-repeat: no-repeat;
+}
+
+.avatar-sm {
+ width: 1.5rem;
+ height: 1.5rem;
+}
+
+/* Cards */
+.card {
+ background-color: #fff;
+ border: var(--tblr-card-border-width) solid var(--tblr-border-color);
+ border-radius: var(--tblr-card-border-radius);
+ box-shadow: var(--tblr-card-box-shadow);
+ margin-bottom: 1.5rem;
+ transition: box-shadow 0.15s ease-in-out;
+}
+
+.card:hover {
+ box-shadow: var(--tblr-box-shadow-lg);
+}
+
+.card-md {
+ max-width: 28rem;
+}
+
+.card-header {
+ padding: 1rem 1.5rem;
+ border-bottom: 1px solid var(--tblr-border-color);
+ background-color: transparent;
+}
+
+.card-title {
+ font-size: 1.125rem;
+ font-weight: var(--tblr-heading-font-weight);
+ margin: 0;
+ color: var(--tblr-dark);
+ display: flex;
+ align-items: center;
+}
+
+.card-body {
+ padding: 1.5rem;
+}
+
+.card-actions {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+/* Stats Cards */
+.h3 {
+ font-size: 1.75rem;
+ font-weight: var(--tblr-heading-font-weight);
+ margin: 0;
+ color: var(--tblr-dark);
+}
+
+.mb-1 {
+ margin-bottom: 0.25rem !important;
+}
+
+/* Buttons */
+.btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ padding: var(--tblr-btn-padding-y) var(--tblr-btn-padding-x);
+ font-size: var(--tblr-font-size-base);
+ font-weight: var(--tblr-btn-font-weight);
+ line-height: 1.5;
+ border: 1px solid transparent;
+ border-radius: var(--tblr-btn-border-radius);
+ cursor: pointer;
+ transition: all 0.15s ease-in-out;
+ text-decoration: none;
+ white-space: nowrap;
+}
+
+.btn:hover {
+ text-decoration: none;
+ transform: translateY(-1px);
+ box-shadow: var(--tblr-box-shadow);
+}
+
+.btn:disabled {
+ opacity: 0.65;
+ cursor: not-allowed;
+ transform: none;
+ box-shadow: none;
+}
+
+.btn-primary {
+ background-color: var(--tblr-primary);
+ border-color: var(--tblr-primary);
+ color: #fff;
+}
+
+.btn-primary:hover {
+ background-color: #1a5aa3;
+ border-color: #1a5aa3;
+ color: #fff;
+}
+
+.btn-secondary {
+ background-color: var(--tblr-secondary);
+ border-color: var(--tblr-secondary);
+ color: #fff;
+}
+
+.btn-secondary:hover {
+ background-color: #5a6268;
+ border-color: #5a6268;
+ color: #fff;
+}
+
+.btn-success {
+ background-color: var(--tblr-success);
+ border-color: var(--tblr-success);
+ color: #fff;
+}
+
+.btn-success:hover {
+ background-color: #28a745;
+ border-color: #28a745;
+ color: #fff;
+}
+
+.btn-warning {
+ background-color: var(--tblr-warning);
+ border-color: var(--tblr-warning);
+ color: #fff;
+}
+
+.btn-warning:hover {
+ background-color: #e0a800;
+ border-color: #e0a800;
+ color: #fff;
+}
+
+.btn-danger {
+ background-color: var(--tblr-danger);
+ border-color: var(--tblr-danger);
+ color: #fff;
+}
+
+.btn-danger:hover {
+ background-color: #c82333;
+ border-color: #c82333;
+ color: #fff;
+}
+
+.btn-outline-primary {
+ background-color: transparent;
+ border-color: var(--tblr-primary);
+ color: var(--tblr-primary);
+}
+
+.btn-outline-primary:hover {
+ background-color: var(--tblr-primary);
+ border-color: var(--tblr-primary);
+ color: #fff;
+}
+
+.btn-outline-secondary {
+ background-color: transparent;
+ border-color: var(--tblr-secondary);
+ color: var(--tblr-secondary);
+}
+
+.btn-outline-secondary:hover {
+ background-color: var(--tblr-secondary);
+ border-color: var(--tblr-secondary);
+ color: #fff;
+}
+
+.btn-outline-danger {
+ background-color: transparent;
+ border-color: var(--tblr-danger);
+ color: var(--tblr-danger);
+}
+
+.btn-outline-danger:hover {
+ background-color: var(--tblr-danger);
+ border-color: var(--tblr-danger);
+ color: #fff;
+}
+
+.btn-sm {
+ padding: 0.25rem 0.5rem;
+ font-size: 0.75rem;
+}
+
+.btn-list {
+ display: flex;
+ gap: 0.25rem;
+}
+
+/* Forms */
+.form-label {
+ font-weight: 500;
+ color: var(--tblr-dark);
+ margin-bottom: 0.5rem;
+}
+
+.form-control {
+ display: block;
+ width: 100%;
+ padding: var(--tblr-input-padding-y) var(--tblr-input-padding-x);
+ font-size: var(--tblr-font-size-base);
+ line-height: var(--tblr-line-height-base);
+ color: var(--tblr-body-color);
+ background-color: #fff;
+ border: 1px solid var(--tblr-border-color);
+ border-radius: var(--tblr-input-border-radius);
+ transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
+}
+
+.form-control:focus {
+ border-color: var(--tblr-primary);
+ outline: 0;
+ box-shadow: 0 0 0 0.2rem rgba(var(--tblr-primary-rgb), 0.25);
+}
+
+.form-control-sm {
+ padding: 0.25rem 0.5rem;
+ font-size: 0.75rem;
+}
+
+.form-footer {
+ margin-top: 1rem;
+ padding-top: 1rem;
+ border-top: 1px solid var(--tblr-border-color);
+}
+
+/* Input Icon */
+.input-icon {
+ position: relative;
+}
+
+.input-icon .icon {
+ position: absolute;
+ left: 0.75rem;
+ top: 50%;
+ transform: translateY(-50%);
+ color: var(--tblr-secondary);
+ z-index: 1;
+}
+
+.input-icon .form-control {
+ padding-left: 2.5rem;
+}
+
+/* Tables */
+.table {
+ width: 100%;
+ margin-bottom: 1rem;
+ color: var(--tblr-body-color);
+ border-collapse: collapse;
+}
+
+.table th,
+.table td {
+ padding: 0.75rem;
+ vertical-align: top;
+ border-top: 1px solid var(--tblr-border-color);
+}
+
+.table thead th {
+ vertical-align: bottom;
+ border-bottom: 2px solid var(--tblr-border-color);
+ font-weight: 600;
+ color: var(--tblr-dark);
+ background-color: var(--tblr-light);
+}
+
+.table tbody tr:hover {
+ background-color: rgba(var(--tblr-primary-rgb), 0.05);
+}
+
+.table-vcenter td {
+ vertical-align: middle;
+}
+
+.card-table {
+ margin-bottom: 0;
+}
+
+.card-table th:first-child,
+.card-table td:first-child {
+ padding-left: 1.5rem;
+}
+
+.card-table th:last-child,
+.card-table td:last-child {
+ padding-right: 1.5rem;
+}
+
+/* Badges */
+.badge {
+ display: inline-block;
+ padding: 0.25rem 0.5rem;
+ font-size: 0.75rem;
+ font-weight: 500;
+ line-height: 1;
+ text-align: center;
+ white-space: nowrap;
+ vertical-align: baseline;
+ border-radius: var(--tblr-border-radius-sm);
+}
+
+.bg-blue-lt {
+ background-color: rgba(var(--tblr-primary-rgb), 0.1);
+ color: var(--tblr-primary);
+}
+
+/* Alerts */
+.alert {
+ position: relative;
+ padding: 1rem 1.5rem;
+ margin-bottom: 1rem;
+ border: 1px solid transparent;
+ border-radius: var(--tblr-border-radius);
+}
+
+.alert-danger {
+ color: #721c24;
+ background-color: #f8d7da;
+ border-color: #f5c6cb;
+}
+
+.alert-success {
+ color: #155724;
+ background-color: #d4edda;
+ border-color: #c3e6cb;
+}
+
+.alert-dismissible {
+ padding-right: 3rem;
+}
+
+.btn-close {
+ position: absolute;
+ top: 0;
+ right: 0;
+ z-index: 2;
+ padding: 1rem 1.5rem;
+ background: transparent;
+ border: 0;
+ cursor: pointer;
+}
+
+.btn-close::before {
+ content: "×";
+ font-size: 1.5rem;
+ font-weight: 700;
+ line-height: 1;
+ color: #000;
+ opacity: 0.5;
+}
+
+.btn-close:hover::before {
+ opacity: 0.75;
+}
+
+.alert-icon {
+ width: 1.25rem;
+ height: 1.25rem;
+ margin-right: 0.5rem;
+}
+
+/* Empty States */
+.empty {
+ text-align: center;
+ padding: 3rem 1rem;
+}
+
+.empty-icon {
+ margin-bottom: 1rem;
+}
+
+.empty-icon .icon {
+ width: 3rem;
+ height: 3rem;
+ color: var(--tblr-secondary);
+}
+
+.empty-title {
+ font-size: 1.25rem;
+ font-weight: var(--tblr-heading-font-weight);
+ color: var(--tblr-dark);
+ margin: 0 0 0.5rem 0;
+}
+
+.empty-subtitle {
+ font-size: var(--tblr-font-size-base);
+ margin: 0;
+}
+
+/* Icons */
+.icon {
+ width: 1rem;
+ height: 1rem;
+ display: inline-block;
+ vertical-align: middle;
+}
+
+/* Modal */
+.modal-backdrop {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background-color: rgba(0, 0, 0, 0.5);
+ z-index: 1050;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ backdrop-filter: blur(2px);
+}
+
+.modal-dialog {
+ max-width: 500px;
+ width: 90%;
+ margin: 1.75rem auto;
+ animation: modalFadeIn 0.3s ease-out;
+}
+
+.modal-content {
+ background-color: #fff;
+ border-radius: var(--tblr-card-border-radius);
+ box-shadow: var(--tblr-box-shadow-lg);
+ border: none;
+}
+
+.modal-header {
+ padding: 1rem 1.5rem;
+ border-bottom: 1px solid var(--tblr-border-color);
+ display: flex;
+ align-items: center;
+}
+
+.modal-title {
+ font-size: 1.125rem;
+ font-weight: var(--tblr-heading-font-weight);
+ margin: 0;
+ color: var(--tblr-dark);
+}
+
+.modal-body {
+ padding: 1.5rem;
+}
+
+.modal-footer {
+ padding: 1rem 1.5rem;
+ border-top: 1px solid var(--tblr-border-color);
+ display: flex;
+ gap: 0.5rem;
+ justify-content: flex-end;
+}
+
+@keyframes modalFadeIn {
+ from {
+ opacity: 0;
+ transform: translateY(-20px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+/* Utilities */
+.text-muted {
+ color: var(--tblr-secondary) !important;
+}
+
+.text-center {
+ text-align: center !important;
+}
+
+.font-weight-medium {
+ font-weight: 500 !important;
+}
+
+.w-1 {
+ width: 1rem !important;
+}
+
+.me-2 {
+ margin-right: 0.5rem !important;
+}
+
+.mb-0 {
+ margin-bottom: 0 !important;
+}
+
+.mb-3 {
+ margin-bottom: 1rem !important;
+}
+
+.mt-1 {
+ margin-top: 0.25rem !important;
+}
+
+.mt-3 {
+ margin-top: 1rem !important;
+}
+
+.py-4 {
+ padding-top: 1.5rem !important;
+ padding-bottom: 1.5rem !important;
+}
+
+/* Footer */
+.footer {
+ background-color: transparent;
+ border-top: 1px solid var(--tblr-border-color);
+ padding: 1rem 0;
+ margin-top: auto;
+}
+
+.footer-transparent {
+ background-color: transparent;
+}
+
+.list-inline {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
+
+.list-inline-item {
+ display: inline-block;
+}
+
+.list-inline-item:not(:last-child) {
+ margin-right: 0.5rem;
+}
+
+.list-inline-dots .list-inline-item:not(:last-child)::after {
+ content: "·";
+ margin-left: 0.5rem;
+ color: var(--tblr-secondary);
+}
+
+.link-secondary {
+ color: var(--tblr-secondary);
+ text-decoration: none;
+}
+
+.link-secondary:hover {
+ color: var(--tblr-dark);
+ text-decoration: underline;
+}
+
+/* Responsive */
+@media (max-width: 768px) {
+ .navbar-expand-md .navbar-collapse {
+ display: none;
+ }
+
+ .navbar-expand-md.show .navbar-collapse {
+ display: block;
+ position: absolute;
+ top: 100%;
+ left: 0;
+ right: 0;
+ background-color: #fff;
+ border-top: 1px solid var(--tblr-border-color);
+ box-shadow: var(--tblr-box-shadow);
+ z-index: 1000;
+ }
+
+ .navbar-expand-md .navbar-nav {
+ flex-direction: column;
+ }
+
+ .navbar-expand-md .nav-item {
+ width: 100%;
+ }
+
+ .navbar-expand-md .nav-link {
+ padding: 1rem;
+ border-radius: 0;
+ }
+
+ .card-actions {
+ flex-direction: column;
+ gap: 0.5rem;
+ }
+
+ .btn-list {
+ flex-direction: column;
+ }
+
+ .modal-footer {
+ flex-direction: column;
+ }
+
+ .modal-footer .btn {
+ width: 100%;
+ }
+}
+
+/* Container */
+.container-xl {
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: 0 1rem;
+}
+
+/* Row and Col */
+.row {
+ display: flex;
+ flex-wrap: wrap;
+ margin: 0 -0.75rem;
+}
+
+.col,
+.col-lg-4,
+.col-lg-8 {
+ padding: 0 0.75rem;
+}
+
+.col {
+ flex: 1;
+}
+
+.col-lg-4 {
+ flex: 0 0 33.333333%;
+ max-width: 33.333333%;
+}
+
+.col-lg-8 {
+ flex: 0 0 66.666667%;
+ max-width: 66.666667%;
+}
+
+@media (max-width: 992px) {
+ .col-lg-4,
+ .col-lg-8 {
+ flex: 0 0 100%;
+ max-width: 100%;
+ }
+}
+
+/* Spinner */
+.spinner-border {
+ display: inline-block;
+ width: 2rem;
+ height: 2rem;
+ border: 0.25em solid currentColor;
+ border-right-color: transparent;
+ border-radius: 50%;
+ animation: spinner-border 0.75s linear infinite;
+}
+
+@keyframes spinner-border {
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+.text-primary {
+ color: var(--tblr-primary) !important;
+}
+
+.visually-hidden {
+ position: absolute !important;
+ width: 1px !important;
+ height: 1px !important;
+ padding: 0 !important;
+ margin: -1px !important;
+ overflow: hidden !important;
+ clip: rect(0, 0, 0, 0) !important;
+ white-space: nowrap !important;
+ border: 0 !important;
+}
+
+/* Enhanced hover effects */
+.card:hover .card-title {
+ color: var(--tblr-primary);
+}
+
+.btn:hover {
+ transform: translateY(-1px);
+ box-shadow: var(--tblr-box-shadow);
+}
+
+.form-control:focus {
+ transform: translateY(-1px);
+ box-shadow: 0 0 0 0.2rem rgba(var(--tblr-primary-rgb), 0.25), var(--tblr-box-shadow);
+}
+
+/* Loading states */
+.btn:disabled {
+ transform: none !important;
+ box-shadow: none !important;
+}
+
+/* Focus states for accessibility */
+.btn:focus,
+.form-control:focus,
+.nav-link:focus {
+ outline: 2px solid var(--tblr-primary);
+ outline-offset: 2px;
+}
+
+/* Dark mode support (future enhancement) */
+@media (prefers-color-scheme: dark) {
+ :root {
+ --tblr-body-bg: #1a1a1a;
+ --tblr-body-color: #e9ecef;
+ --tblr-dark: #f8f9fa;
+ --tblr-border-color: #2d3748;
+ }
+
+ .card {
+ background-color: #2d3748;
+ border-color: #4a5568;
+ }
+
+ .navbar {
+ background-color: #2d3748;
+ border-color: #4a5568;
+ }
+
+ .form-control {
+ background-color: #4a5568;
+ border-color: #718096;
+ color: #e9ecef;
+ }
+
+ .table {
+ color: #e9ecef;
+ }
+
+ .table thead th {
+ background-color: #4a5568;
+ color: #f8f9fa;
+ }
+}
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index 49d5c06..f8cd04f 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -1,40 +1,149 @@
+import { useState } from 'react';
import { Container, Navbar, Tabs, Tab } from 'react-bootstrap';
+import {
+ IconBrandTabler,
+ IconWorld,
+ IconNetwork,
+ IconSettings,
+ IconMenu2,
+ IconX
+} from '@tabler/icons-react';
import DataManager from './DataManager';
+import './App.css';
function App() {
- return (
- <>
-
-
-
-
- S3 Lists Manager
-
-
-
+ const [sidebarOpen, setSidebarOpen] = useState(false);
+ const [activeTab, setActiveTab] = useState('domains');
-
-
-
-
-
-
-
-
-
-
- >
+ const toggleSidebar = () => setSidebarOpen(!sidebarOpen);
+
+ return (
+
+ {/* Header */}
+
+
+
+
+
+ S3 Lists Manager
+
+
+
+
+
+ {/* Sidebar */}
+
+
+
+
+ {/* Main Content */}
+
+
+
+
+
+
+ {activeTab === 'domains' ? 'Управление доменами' : 'Управление AS'}
+
+
+ {activeTab === 'domains'
+ ? 'Добавляйте, редактируйте и управляйте списками доменов'
+ : 'Добавляйте, редактируйте и управляйте списками AS'
+ }
+
+
+
+
+
+
+
+
+ {activeTab === 'domains' && (
+
+ )}
+ {activeTab === 'asns' && (
+
+ )}
+
+
+
+ {/* Footer */}
+
+
+
);
}
diff --git a/frontend/src/DataManager.jsx b/frontend/src/DataManager.jsx
index b92e333..82b10b1 100644
--- a/frontend/src/DataManager.jsx
+++ b/frontend/src/DataManager.jsx
@@ -1,6 +1,20 @@
import { useState, useEffect } from 'react';
import axios from 'axios';
import { Table, Button, Form, Row, Col, Card, Alert } from 'react-bootstrap';
+import {
+ IconPlus,
+ IconSearch,
+ IconEdit,
+ IconTrash,
+ IconCheck,
+ IconX,
+ IconDatabase,
+ IconRefresh,
+ IconArrowsExchange,
+ IconDownload,
+ IconUpload,
+ IconAlertTriangle
+} from '@tabler/icons-react';
const API_URL = '/api';
@@ -14,18 +28,25 @@ function DataManager({ entityName, entityKey, placeholder }) {
const [editingValue, setEditingValue] = useState('');
const [bulkFrom, setBulkFrom] = useState('SharkFIN');
const [bulkTo, setBulkTo] = useState('SharkAM');
+ const [loading, setLoading] = useState(false);
+ const [showDeleteModal, setShowDeleteModal] = useState(false);
+ const [itemToDelete, setItemToDelete] = useState(null);
useEffect(() => {
fetchItems();
}, []);
const fetchItems = async () => {
+ setLoading(true);
try {
const response = await axios.get(`${API_URL}/${entityKey}`);
setItems(response.data);
+ setError('');
} catch (error) {
console.error(`Error fetching ${entityKey}:`, error);
setError(`Не удалось загрузить ${entityName}. Проверьте, запущен ли бэкенд.`);
+ } finally {
+ setLoading(false);
}
};
@@ -60,15 +81,30 @@ function DataManager({ entityName, entityKey, placeholder }) {
setItems(items.filter(i => i.domain !== domainNameToDelete));
};
+ const confirmDelete = (item) => {
+ setItemToDelete(item);
+ setShowDeleteModal(true);
+ };
+
+ const executeDelete = () => {
+ if (itemToDelete) {
+ handleDeleteItem(itemToDelete.domain);
+ setShowDeleteModal(false);
+ setItemToDelete(null);
+ }
+ };
+
const handleSaveChanges = async () => {
+ setLoading(true);
try {
- // The backend expects a payload with a 'domains' key for both endpoints.
await axios.post(`${API_URL}/${entityKey}`, { domains: items });
setSuccess('Изменения успешно сохранены!');
setTimeout(() => setSuccess(''), 3000);
} catch (error) {
console.error('Error saving changes:', error);
setError('Не удалось сохранить изменения.');
+ } finally {
+ setLoading(false);
}
};
@@ -93,130 +129,367 @@ function DataManager({ entityName, entityKey, placeholder }) {
return (
<>
- {error && setError('')} dismissible>{error}}
- {success && setSuccess('')} dismissible>{success}}
+ {error && (
+
+ )}
+
+ {success && (
+
+ )}
-
-
-
- Добавить новый {entityName}
-
-
- Имя {entityName}
-
+
+ {/* Add New Item Card */}
+
+
+
+
+ Добавить новый {entityName}
+
+
+
-
- Массовое обновление
-
-
- Заменить с
- setBulkFrom(e.target.value)} />
-
-
- Заменить на
- setBulkTo(e.target.value)} />
-
-
-
-
-
-
-
-
-
-
-
Список: {entityName}
-
+ {/* Bulk Update Card */}
+
+
+
+
+ Массовое обновление
+
+
+
+
+
+ setBulkFrom(e.target.value)}
+ />
-
-
- setSearchTerm(e.target.value)}
- />
-
-
-
- | # |
- {entityName} |
- Шлюз |
- Действия |
-
-
-
- {filteredItems.map((item, index) => (
-
- | {index + 1} |
- {item.domain} |
-
- {editingDomain === item.domain ? (
- setEditingValue(e.target.value)}
- autoFocus
- />
- ) : (
- item.type
- )}
- |
-
- {editingDomain === item.domain ? (
- <>
-
-
- >
- ) : (
- <>
-
-
- >
- )}
- |
+
+
+ setBulkTo(e.target.value)}
+ />
+
+
+
+
+
+
+
+ {/* Stats Card */}
+
+
+
+
+ Статистика
+
+
+
+
+
+
Всего
+
{items.length}
+
+
+
Найдено
+
{filteredItems.length}
+
+
+
+
+
+
+
+ {/* Main Data Card */}
+
+
+
+
+
+ Список: {entityName}
+
+
+
+
+
+
+
+
+ {/* Search */}
+
+
+
+ setSearchTerm(e.target.value)}
+ />
+
+
+
+ {/* Table */}
+
+
+
+
+ | # |
+ {entityName} |
+ Шлюз |
+ |
- ))}
-
-
+
+
+ {filteredItems.map((item, index) => (
+
+ | {index + 1} |
+
+ {item.domain}
+ |
+
+ {editingDomain === item.domain ? (
+ setEditingValue(e.target.value)}
+ autoFocus
+ />
+ ) : (
+ {item.type}
+ )}
+ |
+
+ {editingDomain === item.domain ? (
+
+
+
+
+ ) : (
+
+
+
+
+ )}
+ |
+
+ ))}
+
+
+
+
+ {/* Empty States */}
{items.length > 0 && filteredItems.length === 0 && (
-
По вашему запросу ничего не найдено.
+
+
+
+
+
Ничего не найдено
+
+ По вашему запросу ничего не найдено.
+
+
)}
- {items.length === 0 &&
Список пуст. Добавьте новый элемент.
}
-
-
-
-
+
+ {items.length === 0 && !loading && (
+
+
+
+
+
Список пуст
+
+ Добавьте новый элемент, чтобы начать работу.
+
+
+ )}
+
+ {loading && (
+
+ )}
+
+
+
+
+
+ {/* Delete Confirmation Modal */}
+ {showDeleteModal && (
+
+
+
+
+
+
Подтверждение удаления
+
+
+
+
Вы уверены, что хотите удалить {itemToDelete?.domain}?
+
Это действие нельзя отменить.
+
+
+
+
+
+
+
+
+ )}
>
);
}