feat: Integrate @tabler/core for UI enhancements and refactor App component with new sidebar and dashboard features
Publish Tabler Docker image / build-and-push-tabler (push) Has been cancelled

This commit is contained in:
2025-07-08 23:02:32 +07:00
parent 0c1c156bb7
commit 1eb3e9d3f6
6 changed files with 466 additions and 1160 deletions
+37
View File
@@ -8,6 +8,7 @@
"name": "frontend", "name": "frontend",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@tabler/core": "^1.3.2",
"@tabler/icons-react": "^3.34.0", "@tabler/icons-react": "^3.34.0",
"axios": "^1.10.0", "axios": "^1.10.0",
"bootstrap": "^5.3.7", "bootstrap": "^5.3.7",
@@ -642,6 +643,42 @@
"tslib": "^2.8.0" "tslib": "^2.8.0"
} }
}, },
"node_modules/@tabler/core": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@tabler/core/-/core-1.3.2.tgz",
"integrity": "sha512-QDVJbv48YJrahBLdxYkLi6NutQv4jGbkUWyzxE2NcNJ3s3GGpRx98JmbAoN92NZKNmf26vZdW6k2Q5haVKlS4A==",
"license": "MIT",
"dependencies": {
"@popperjs/core": "^2.11.8",
"bootstrap": "5.3.6"
},
"engines": {
"node": ">=20"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/codecalm"
}
},
"node_modules/@tabler/core/node_modules/bootstrap": {
"version": "5.3.6",
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.6.tgz",
"integrity": "sha512-jX0GAcRzvdwISuvArXn3m7KZscWWFAf1MKBcnzaN02qWMb3jpMoUX4/qgeiGzqyIb4ojulRzs89UCUmGcFSzTA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/twbs"
},
{
"type": "opencollective",
"url": "https://opencollective.com/bootstrap"
}
],
"license": "MIT",
"peerDependencies": {
"@popperjs/core": "^2.11.8"
}
},
"node_modules/@tabler/icons": { "node_modules/@tabler/icons": {
"version": "3.34.0", "version": "3.34.0",
"resolved": "https://registry.npmjs.org/@tabler/icons/-/icons-3.34.0.tgz", "resolved": "https://registry.npmjs.org/@tabler/icons/-/icons-3.34.0.tgz",
+1
View File
@@ -10,6 +10,7 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@tabler/core": "^1.3.2",
"@tabler/icons-react": "^3.34.0", "@tabler/icons-react": "^3.34.0",
"axios": "^1.10.0", "axios": "^1.10.0",
"bootstrap": "^5.3.7", "bootstrap": "^5.3.7",
+34 -931
View File
File diff suppressed because it is too large Load Diff
+284 -60
View File
@@ -6,36 +6,96 @@ import {
IconNetwork, IconNetwork,
IconSettings, IconSettings,
IconMenu2, IconMenu2,
IconX IconX,
IconHome,
IconDatabase,
IconUsers,
IconBell,
IconSearch,
IconChevronDown,
IconLogout,
IconUser,
IconDashboard,
IconFileText,
IconCloud,
IconShield,
IconActivity
} from '@tabler/icons-react'; } from '@tabler/icons-react';
import DataManager from './DataManager'; import DataManager from './DataManager';
import './App.css'; import './App.css';
function App() { function App() {
const [sidebarOpen, setSidebarOpen] = useState(false); const [sidebarOpen, setSidebarOpen] = useState(false);
const [activeTab, setActiveTab] = useState('domains'); const [activeTab, setActiveTab] = useState('dashboard');
const [notifications] = useState([
{ id: 1, title: 'Новый домен добавлен', message: 'example.com был добавлен в список', time: '2 мин назад' },
{ id: 2, title: 'Изменения сохранены', message: 'Список AS обновлен в S3', time: '5 мин назад' }
]);
const toggleSidebar = () => setSidebarOpen(!sidebarOpen); const toggleSidebar = () => setSidebarOpen(!sidebarOpen);
const sidebarItems = [
{ id: 'dashboard', title: 'Dashboard', icon: IconDashboard, badge: null },
{ id: 'domains', title: 'Домены', icon: IconWorld, badge: '12' },
{ id: 'asns', title: 'AS', icon: IconNetwork, badge: '8' },
{ id: 'files', title: 'Файлы', icon: IconFileText, badge: null },
{ id: 'cloud', title: 'Облако', icon: IconCloud, badge: null },
{ id: 'security', title: 'Безопасность', icon: IconShield, badge: null },
{ id: 'activity', title: 'Активность', icon: IconActivity, badge: '3' },
{ id: 'settings', title: 'Настройки', icon: IconSettings, badge: null }
];
const renderContent = () => {
switch (activeTab) {
case 'dashboard':
return <Dashboard />;
case 'domains':
return (
<DataManager
entityName="домен"
entityKey="domains"
placeholder="example.com"
/>
);
case 'asns':
return (
<DataManager
entityName="AS"
entityKey="asns"
placeholder="AS12345"
/>
);
default:
return <Dashboard />;
}
};
const getPageTitle = () => {
const item = sidebarItems.find(item => item.id === activeTab);
return item ? item.title : 'Dashboard';
};
return ( return (
<div className="page"> <div className="page">
{/* Header */} {/* Sidebar */}
<header className="navbar navbar-expand-md navbar-light d-print-none"> <aside className={`navbar navbar-vertical navbar-expand-lg ${sidebarOpen ? 'navbar-show' : ''}`}>
<div className="container-xl"> <div className="container-fluid">
<button <button
className="navbar-toggler" className="navbar-toggler"
type="button" type="button"
onClick={toggleSidebar} onClick={toggleSidebar}
> >
<IconMenu2 className="icon" /> <IconX className="icon" />
</button> </button>
<h1 className="navbar-brand navbar-brand-autodark d-none-navbar-horizontal pe-0 pe-md-3">
<h1 className="navbar-brand navbar-brand-autodark">
<IconBrandTabler className="navbar-brand-icon me-2" /> <IconBrandTabler className="navbar-brand-icon me-2" />
S3 Lists Manager S3 Lists Manager
</h1> </h1>
<div className="navbar-nav flex-row order-md-last">
<div className="navbar-nav flex-row d-lg-none">
<div className="nav-item dropdown"> <div className="nav-item dropdown">
<a href="#" className="nav-link d-flex lh-1 text-reset p-0" data-bs-toggle="dropdown" aria-label="Open user menu"> <a href="#" className="nav-link d-flex lh-1 text-reset p-0" data-bs-toggle="dropdown">
<span className="avatar avatar-sm" style={{backgroundImage: 'url(./static/avatars/000m.jpg)'}}></span> <span className="avatar avatar-sm" style={{backgroundImage: 'url(./static/avatars/000m.jpg)'}}></span>
</a> </a>
<div className="dropdown-menu dropdown-menu-end dropdown-menu-arrow"> <div className="dropdown-menu dropdown-menu-end dropdown-menu-arrow">
@@ -46,83 +106,164 @@ function App() {
</div> </div>
</div> </div>
</div> </div>
</div>
</header>
{/* Sidebar */} <div className="collapse navbar-collapse" id="sidebar-menu">
<div className={`navbar-expand-md ${sidebarOpen ? 'show' : ''}`}> <ul className="navbar-nav pt-lg-auto">
<div className="collapse navbar-collapse" id="navbar-menu"> {sidebarItems.map((item) => (
<div className="navbar navbar-light"> <li key={item.id} className="nav-item">
<div className="container-xl">
<ul className="navbar-nav">
<li className="nav-item">
<a <a
className={`nav-link ${activeTab === 'domains' ? 'active' : ''}`} className={`nav-link ${activeTab === item.id ? 'active' : ''}`}
href="#" href="#"
onClick={(e) => { e.preventDefault(); setActiveTab('domains'); }} onClick={(e) => { e.preventDefault(); setActiveTab(item.id); }}
> >
<IconWorld className="nav-link-icon" /> <item.icon className="nav-link-icon" />
<span className="nav-link-title">Домены</span> <span className="nav-link-title">{item.title}</span>
{item.badge && (
<span className="badge badge-sm bg-green-lt text-green ms-auto">
{item.badge}
</span>
)}
</a> </a>
</li> </li>
<li className="nav-item"> ))}
<a </ul>
className={`nav-link ${activeTab === 'asns' ? 'active' : ''}`} </div>
href="#" </div>
onClick={(e) => { e.preventDefault(); setActiveTab('asns'); }} </aside>
>
<IconNetwork className="nav-link-icon" /> {/* Header */}
<span className="nav-link-title">AS</span> <header className="navbar navbar-expand-md navbar-light d-none d-lg-flex d-print-none">
</a> <div className="container-xl">
</li> <button
<li className="nav-item"> className="navbar-toggler"
<a className="nav-link" href="#"> type="button"
<IconSettings className="nav-link-icon" /> onClick={toggleSidebar}
<span className="nav-link-title">Настройки</span> >
</a> <IconMenu2 className="icon" />
</li> </button>
</ul>
<div className="navbar-nav flex-row order-md-last">
<div className="nav-item dropdown d-none d-md-flex me-3">
<a href="#" className="nav-link px-0" data-bs-toggle="dropdown" tabIndex="-1">
<IconBell className="icon" />
<span className="badge badge-sm bg-red"></span>
</a>
<div className="dropdown-menu dropdown-menu-arrow dropdown-menu-end dropdown-menu-card">
<div className="card">
<div className="card-header">
<h3 className="card-title">Уведомления</h3>
</div>
<div className="list-group list-group-flush">
{notifications.map((notification) => (
<div key={notification.id} className="list-group-item">
<div className="row align-items-center">
<div className="col-auto">
<span className="status-dot status-dot-animated bg-red d-block"></span>
</div>
<div className="col text-truncate">
<a href="#" className="text-body d-block">{notification.title}</a>
<div className="d-block text-muted text-truncate mt-n1">
{notification.message}
</div>
</div>
<div className="col-auto">
<a href="#" className="list-group-item-actions">
<IconX className="icon" />
</a>
</div>
</div>
</div>
))}
</div>
</div>
</div>
</div>
<div className="nav-item dropdown">
<a href="#" className="nav-link d-flex lh-1 text-reset p-0" data-bs-toggle="dropdown">
<span className="avatar avatar-sm" style={{backgroundImage: 'url(./static/avatars/000m.jpg)'}}></span>
<span className="d-none d-xl-block ps-2">
<span>Admin User</span>
<IconChevronDown className="icon" />
</span>
</a>
<div className="dropdown-menu dropdown-menu-end dropdown-menu-arrow">
<a href="#" className="dropdown-item">
<IconUser className="icon me-2" />
Profile
</a>
<a href="#" className="dropdown-item">
<IconSettings className="icon me-2" />
Settings
</a>
<div className="dropdown-divider"></div>
<a href="#" className="dropdown-item">
<IconLogout className="icon me-2" />
Logout
</a>
</div>
</div>
</div>
<div className="navbar-nav flex-row">
<div className="nav-item d-none d-md-flex me-3">
<div className="input-icon">
<IconSearch className="icon" />
<input type="text" className="form-control" placeholder="Поиск..." />
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </header>
{/* Main Content */} {/* Main Content */}
<div className="page-wrapper"> <div className="page-wrapper">
<div className="container-xl"> <div className="container-xl">
{/* Page Header */}
<div className="page-header d-print-none"> <div className="page-header d-print-none">
<div className="row align-items-center"> <div className="row align-items-center">
<div className="col"> <div className="col">
<div className="page-pretitle">
S3 Lists Manager
</div>
<h2 className="page-title"> <h2 className="page-title">
{activeTab === 'domains' ? 'Управление доменами' : 'Управление AS'} {getPageTitle()}
</h2> </h2>
<div className="text-muted mt-1"> </div>
{activeTab === 'domains' <div className="col-auto ms-auto d-print-none">
? 'Добавляйте, редактируйте и управляйте списками доменов' <div className="btn-list">
: 'Добавляйте, редактируйте и управляйте списками AS' <span className="d-none d-sm-inline">
} <a href="#" className="btn btn-white">
Новый домен
</a>
</span>
<a href="#" className="btn btn-primary d-none d-sm-inline-block">
<IconDatabase className="icon me-2" />
Сохранить в S3
</a>
</div> </div>
</div> </div>
</div> </div>
{/* Breadcrumbs */}
<div className="page-header-breadcrumb">
<ol className="breadcrumb">
<li className="breadcrumb-item">
<a href="#" onClick={(e) => { e.preventDefault(); setActiveTab('dashboard'); }}>
<IconHome className="icon me-2" />
Главная
</a>
</li>
<li className="breadcrumb-item active" aria-current="page">
{getPageTitle()}
</li>
</ol>
</div>
</div> </div>
</div> </div>
<div className="page-body"> <div className="page-body">
<div className="container-xl"> <div className="container-xl">
{activeTab === 'domains' && ( {renderContent()}
<DataManager
entityName="домен"
entityKey="domains"
placeholder="example.com"
/>
)}
{activeTab === 'asns' && (
<DataManager
entityName="AS"
entityKey="asns"
placeholder="AS12345"
/>
)}
</div> </div>
</div> </div>
@@ -147,4 +288,87 @@ function App() {
); );
} }
// Dashboard Component
function Dashboard() {
const stats = [
{ title: 'Всего доменов', value: '1,234', icon: IconWorld, color: 'blue' },
{ title: 'Всего AS', value: '567', icon: IconNetwork, color: 'green' },
{ title: 'Активные шлюзы', value: '12', icon: IconDatabase, color: 'orange' },
{ title: 'Последние изменения', value: '3', icon: IconActivity, color: 'red' }
];
return (
<div className="row row-deck row-cards">
{/* Stats Cards */}
{stats.map((stat, index) => (
<div key={index} className="col-sm-6 col-lg-3">
<div className="card">
<div className="card-body">
<div className="d-flex align-items-center">
<div className="subheader">{stat.title}</div>
</div>
<div className="h1 mb-3">{stat.value}</div>
<div className="d-flex mb-2">
<stat.icon className={`icon icon-sm text-${stat.color}`} />
</div>
</div>
</div>
</div>
))}
{/* Main Content Card */}
<div className="col-12">
<div className="card">
<div className="card-header">
<h3 className="card-title">Добро пожаловать в S3 Lists Manager</h3>
</div>
<div className="card-body">
<p>Выберите раздел в боковом меню для управления списками доменов и AS.</p>
<div className="row">
<div className="col-md-6">
<div className="card card-sm">
<div className="card-body">
<div className="row align-items-center">
<div className="col-auto">
<IconWorld className="icon icon-lg text-blue" />
</div>
<div className="col">
<div className="font-weight-medium">
Управление доменами
</div>
<div className="text-muted">
Добавляйте, редактируйте и управляйте списками доменов
</div>
</div>
</div>
</div>
</div>
</div>
<div className="col-md-6">
<div className="card card-sm">
<div className="card-body">
<div className="row align-items-center">
<div className="col-auto">
<IconNetwork className="icon icon-lg text-green" />
</div>
<div className="col">
<div className="font-weight-medium">
Управление AS
</div>
<div className="text-muted">
Добавляйте, редактируйте и управляйте списками AS
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
);
}
export default App; export default App;
+92 -152
View File
@@ -1,6 +1,5 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import axios from 'axios'; import axios from 'axios';
import { Table, Button, Form, Row, Col, Card, Alert } from 'react-bootstrap';
import { import {
IconPlus, IconPlus,
IconSearch, IconSearch,
@@ -199,7 +198,7 @@ function DataManager({ entityName, entityKey, placeholder }) {
</div> </div>
{/* Bulk Update Card */} {/* Bulk Update Card */}
<div className="card card-md mt-3"> <div className="card card-md">
<div className="card-header"> <div className="card-header">
<h3 className="card-title"> <h3 className="card-title">
<IconArrowsExchange className="icon me-2" /> <IconArrowsExchange className="icon me-2" />
@@ -207,13 +206,13 @@ function DataManager({ entityName, entityKey, placeholder }) {
</h3> </h3>
</div> </div>
<div className="card-body"> <div className="card-body">
<div className="mb-2"> <div className="mb-3">
<label className="form-label">Заменить с</label> <label className="form-label">Заменить с</label>
<input <input
type="text" type="text"
className="form-control" className="form-control"
value={bulkFrom} value={bulkFrom}
onChange={e => setBulkFrom(e.target.value)} onChange={(e) => setBulkFrom(e.target.value)}
/> />
</div> </div>
<div className="mb-3"> <div className="mb-3">
@@ -222,7 +221,7 @@ function DataManager({ entityName, entityKey, placeholder }) {
type="text" type="text"
className="form-control" className="form-control"
value={bulkTo} value={bulkTo}
onChange={e => setBulkTo(e.target.value)} onChange={(e) => setBulkTo(e.target.value)}
/> />
</div> </div>
<div className="form-footer"> <div className="form-footer">
@@ -232,85 +231,75 @@ function DataManager({ entityName, entityKey, placeholder }) {
onClick={handleBulkUpdate} onClick={handleBulkUpdate}
> >
<IconArrowsExchange className="icon me-2" /> <IconArrowsExchange className="icon me-2" />
Выполнить замену Обновить все
</button> </button>
</div> </div>
</div> </div>
</div> </div>
{/* Stats Card */} {/* Actions Card */}
<div className="card card-md mt-3"> <div className="card card-md">
<div className="card-header"> <div className="card-header">
<h3 className="card-title"> <h3 className="card-title">
<IconDatabase className="icon me-2" /> <IconDatabase className="icon me-2" />
Статистика Действия
</h3> </h3>
</div> </div>
<div className="card-body"> <div className="card-body">
<div className="row text-center"> <div className="d-grid gap-2">
<div className="col"> <button
<div className="text-muted mb-1">Всего</div> className="btn btn-primary"
<div className="h3 mb-0">{items.length}</div> onClick={handleSaveChanges}
</div> disabled={loading}
<div className="col"> >
<div className="text-muted mb-1">Найдено</div> {loading ? (
<div className="h3 mb-0">{filteredItems.length}</div> <>
</div> <span className="spinner-border spinner-border-sm me-2" role="status"></span>
Сохранение...
</>
) : (
<>
<IconDatabase className="icon me-2" />
Сохранить в S3
</>
)}
</button>
<button
className="btn btn-outline-secondary"
onClick={fetchItems}
disabled={loading}
>
<IconRefresh className="icon me-2" />
Обновить
</button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div className="col-lg-8"> <div className="col-lg-8">
{/* Main Data Card */} {/* Search and Table Card */}
<div className="card"> <div className="card">
<div className="card-header"> <div className="card-header">
<div className="d-flex justify-content-between align-items-center"> <h3 className="card-title">Список {entityName}</h3>
<h3 className="card-title"> <div className="card-actions">
<IconDatabase className="icon me-2" />
Список: {entityName}
</h3>
<div className="card-actions">
<button
className="btn btn-outline-secondary me-2"
onClick={fetchItems}
disabled={loading}
>
<IconRefresh className="icon me-2" />
Обновить
</button>
<button
className="btn btn-success"
onClick={handleSaveChanges}
disabled={items.length === 0 || loading}
>
<IconUpload className="icon me-2" />
Сохранить в S3
</button>
</div>
</div>
</div>
<div className="card-body">
{/* Search */}
<div className="mb-3">
<div className="input-icon"> <div className="input-icon">
<IconSearch className="icon" /> <IconSearch className="icon" />
<input <input
type="text" type="text"
className="form-control" className="form-control"
placeholder={`Поиск по ${entityName}...`} placeholder={`Поиск ${entityName}...`}
value={searchTerm} value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)} onChange={(e) => setSearchTerm(e.target.value)}
/> />
</div> </div>
</div> </div>
</div>
{/* Table */} <div className="card-body">
<div className="table-responsive"> <div className="table-responsive">
<table className="table table-vcenter card-table"> <table className="table table-vcenter card-table">
<thead> <thead>
<tr> <tr>
<th>#</th>
<th>{entityName}</th> <th>{entityName}</th>
<th>Шлюз</th> <th>Шлюз</th>
<th className="w-1"></th> <th className="w-1"></th>
@@ -318,10 +307,18 @@ function DataManager({ entityName, entityKey, placeholder }) {
</thead> </thead>
<tbody> <tbody>
{filteredItems.map((item, index) => ( {filteredItems.map((item, index) => (
<tr key={item.domain}> <tr key={index}>
<td className="text-muted">{index + 1}</td>
<td> <td>
<div className="font-weight-medium">{item.domain}</div> {editingDomain === item.domain ? (
<input
type="text"
className="form-control form-control-sm"
value={editingDomain}
onChange={(e) => setEditingDomain(e.target.value)}
/>
) : (
item.domain
)}
</td> </td>
<td> <td>
{editingDomain === item.domain ? ( {editingDomain === item.domain ? (
@@ -330,7 +327,6 @@ function DataManager({ entityName, entityKey, placeholder }) {
className="form-control form-control-sm" className="form-control form-control-sm"
value={editingValue} value={editingValue}
onChange={(e) => setEditingValue(e.target.value)} onChange={(e) => setEditingValue(e.target.value)}
autoFocus
/> />
) : ( ) : (
<span className="badge bg-blue-lt">{item.type}</span> <span className="badge bg-blue-lt">{item.type}</span>
@@ -374,39 +370,17 @@ function DataManager({ entityName, entityKey, placeholder }) {
</tbody> </tbody>
</table> </table>
</div> </div>
{filteredItems.length === 0 && (
{/* Empty States */}
{items.length > 0 && filteredItems.length === 0 && (
<div className="empty"> <div className="empty">
<div className="empty-icon"> <div className="empty-img">
<IconSearch className="icon" /> <IconAlertTriangle className="icon" />
</div> </div>
<p className="empty-title">Ничего не найдено</p> <p className="empty-title">Нет {entityName} для отображения</p>
<p className="empty-subtitle text-muted"> <p className="empty-subtitle text-muted">
По вашему запросу ничего не найдено. Добавьте первый {entityName} используя форму слева.
</p> </p>
</div> </div>
)} )}
{items.length === 0 && !loading && (
<div className="empty">
<div className="empty-icon">
<IconDatabase className="icon" />
</div>
<p className="empty-title">Список пуст</p>
<p className="empty-subtitle text-muted">
Добавьте новый элемент, чтобы начать работу.
</p>
</div>
)}
{loading && (
<div className="text-center py-4">
<div className="spinner-border text-primary" role="status">
<span className="visually-hidden">Загрузка...</span>
</div>
</div>
)}
</div> </div>
</div> </div>
</div> </div>
@@ -414,77 +388,43 @@ function DataManager({ entityName, entityKey, placeholder }) {
{/* Delete Confirmation Modal */} {/* Delete Confirmation Modal */}
{showDeleteModal && ( {showDeleteModal && (
<div className="modal-backdrop" style={{ <div className="modal modal-blur fade show" style={{display: 'block'}} tabIndex="-1">
position: 'fixed', <div className="modal-dialog modal-sm modal-dialog-centered" role="document">
top: 0, <div className="modal-content">
left: 0, <button
right: 0, type="button"
bottom: 0, className="btn-close"
backgroundColor: 'rgba(0, 0, 0, 0.5)', onClick={() => setShowDeleteModal(false)}
zIndex: 1050, ></button>
display: 'flex', <div className="modal-status bg-danger"></div>
alignItems: 'center', <div className="modal-body text-center py-4">
justifyContent: 'center' <IconTrash className="icon mb-2 text-danger icon-lg" />
}}> <h3>Удалить {entityName}?</h3>
<div className="modal-dialog" style={{ <div className="text-muted">
maxWidth: '500px', Вы уверены, что хотите удалить "{itemToDelete?.domain}"? Это действие необратимо.
width: '90%', </div>
margin: '1.75rem auto'
}}>
<div className="modal-content" style={{
backgroundColor: '#fff',
borderRadius: '0.5rem',
boxShadow: '0 0.5rem 1rem rgba(0, 0, 0, 0.15)'
}}>
<div className="modal-header" style={{
padding: '1rem 1.5rem',
borderBottom: '1px solid var(--tblr-border-color)',
display: 'flex',
alignItems: 'center'
}}>
<IconAlertTriangle className="icon me-2" style={{ color: 'var(--tblr-danger)' }} />
<h5 className="modal-title" style={{ margin: 0 }}>Подтверждение удаления</h5>
<button
type="button"
className="btn-close"
onClick={() => setShowDeleteModal(false)}
style={{
background: 'none',
border: 'none',
fontSize: '1.5rem',
cursor: 'pointer',
marginLeft: 'auto'
}}
>
×
</button>
</div> </div>
<div className="modal-body" style={{ padding: '1.5rem' }}> <div className="modal-footer">
<p>Вы уверены, что хотите удалить <strong>{itemToDelete?.domain}</strong>?</p> <div className="w-100">
<p className="text-muted mb-0">Это действие нельзя отменить.</p> <div className="row">
</div> <div className="col">
<div className="modal-footer" style={{ <button
padding: '1rem 1.5rem', className="btn w-100"
borderTop: '1px solid var(--tblr-border-color)', onClick={() => setShowDeleteModal(false)}
display: 'flex', >
gap: '0.5rem', Отмена
justifyContent: 'flex-end' </button>
}}> </div>
<button <div className="col">
type="button" <button
className="btn btn-secondary" className="btn btn-danger w-100"
onClick={() => setShowDeleteModal(false)} onClick={executeDelete}
> >
Отмена Удалить
</button> </button>
<button </div>
type="button" </div>
className="btn btn-danger" </div>
onClick={executeDelete}
>
<IconTrash className="icon me-2" />
Удалить
</button>
</div> </div>
</div> </div>
</div> </div>
+1
View File
@@ -2,6 +2,7 @@ import React from 'react'
import ReactDOM from 'react-dom/client' import ReactDOM from 'react-dom/client'
import App from './App.jsx' import App from './App.jsx'
import './index.css' import './index.css'
import '@tabler/core/dist/css/tabler.min.css'
import 'bootstrap/dist/css/bootstrap.min.css'; import 'bootstrap/dist/css/bootstrap.min.css';
ReactDOM.createRoot(document.getElementById('root')).render( ReactDOM.createRoot(document.getElementById('root')).render(