refactor(CommunitiesManager): simplify layout structure and enhance notification handling for improved user experience
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m49s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m49s
This commit is contained in:
+257
-265
@@ -350,7 +350,7 @@ function CommunitiesManager() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="page-wrapper">
|
<div>
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title="Справочник Community"
|
title="Справочник Community"
|
||||||
icon={<IconFilter size={24} />}
|
icon={<IconFilter size={24} />}
|
||||||
@@ -368,276 +368,268 @@ function CommunitiesManager() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="page-body">
|
{/* Уведомления */}
|
||||||
<div className="container-xl">
|
{error && (
|
||||||
{/* Уведомления */}
|
<ErrorAlert message={error} onClose={() => setError('')} onRetry={fetchItems} />
|
||||||
{error && (
|
)}
|
||||||
<ErrorAlert message={error} onClose={() => setError('')} onRetry={fetchItems} />
|
{success && (
|
||||||
)}
|
<div className="alert alert-success alert-dismissible" role="alert">
|
||||||
{success && (
|
<div className="d-flex">
|
||||||
<div className="alert alert-success alert-dismissible" role="alert">
|
<div>
|
||||||
<div className="d-flex">
|
<IconCheck className="icon alert-icon" />
|
||||||
<div>
|
|
||||||
<IconCheck className="icon alert-icon" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{success}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button type="button" className="btn-close" onClick={() => setSuccess('')}></button>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
<div>
|
||||||
|
{success}
|
||||||
{/* Вкладки: Список / Статистика в стиле Tabler */}
|
|
||||||
<div className="card mb-3">
|
|
||||||
<div className="card-header border-0 pb-0">
|
|
||||||
<ul className="nav nav-tabs card-header-tabs" role="tablist">
|
|
||||||
<li className="nav-item" role="presentation">
|
|
||||||
<button
|
|
||||||
className={`nav-link ${activeTab === 'list' ? 'active' : ''}`}
|
|
||||||
onClick={() => setActiveTab('list')}
|
|
||||||
type="button"
|
|
||||||
role="tab"
|
|
||||||
aria-selected={activeTab === 'list'}
|
|
||||||
>
|
|
||||||
<IconList className="me-2" size={18} />
|
|
||||||
Список communities
|
|
||||||
<span className="badge bg-blue-lt text-blue ms-2">{filtered.length}</span>
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
<li className="nav-item" role="presentation">
|
|
||||||
<button
|
|
||||||
className={`nav-link ${activeTab === 'stats' ? 'active' : ''}`}
|
|
||||||
onClick={() => setActiveTab('stats')}
|
|
||||||
type="button"
|
|
||||||
role="tab"
|
|
||||||
aria-selected={activeTab === 'stats'}
|
|
||||||
>
|
|
||||||
<IconChartBar className="me-2" size={18} />
|
|
||||||
Статистика использования
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<button type="button" className="btn-close" onClick={() => setSuccess('')}></button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Отображение списка communities */}
|
{/* Вкладки: Список / Статистика в стиле Tabler */}
|
||||||
{activeTab === 'list' && (
|
<div className="card card-flat mb-3">
|
||||||
<div className="row g-3">
|
<div className="card-header border-0 pb-0">
|
||||||
<div className="col-12">
|
<ul className="nav nav-tabs card-header-tabs" role="tablist">
|
||||||
<div className="card">
|
<li className="nav-item" role="presentation">
|
||||||
<div className="card-header">
|
<button
|
||||||
<div className="row align-items-center w-100 g-2">
|
className={`nav-link ${activeTab === 'list' ? 'active' : ''}`}
|
||||||
<div className="col">
|
onClick={() => setActiveTab('list')}
|
||||||
<h3 className="card-title mb-0">
|
type="button"
|
||||||
<IconFilter className="icon me-2" size={20} />
|
role="tab"
|
||||||
Список community
|
aria-selected={activeTab === 'list'}
|
||||||
<span className="badge bg-blue-lt text-blue ms-2">{filtered.length}</span>
|
>
|
||||||
</h3>
|
<IconList className="me-2" size={18} />
|
||||||
</div>
|
Список communities
|
||||||
<div className="col-auto">
|
<span className="badge bg-blue-lt text-blue ms-2">{filtered.length}</span>
|
||||||
<button
|
</button>
|
||||||
className="btn btn-primary"
|
</li>
|
||||||
onClick={() => setAddModalOpen(true)}
|
<li className="nav-item" role="presentation">
|
||||||
>
|
<button
|
||||||
<IconPlus className="icon me-1" />
|
className={`nav-link ${activeTab === 'stats' ? 'active' : ''}`}
|
||||||
Добавить community
|
onClick={() => setActiveTab('stats')}
|
||||||
</button>
|
type="button"
|
||||||
</div>
|
role="tab"
|
||||||
</div>
|
aria-selected={activeTab === 'stats'}
|
||||||
</div>
|
>
|
||||||
<div className="card-body">
|
<IconChartBar className="me-2" size={18} />
|
||||||
<div className="d-flex gap-2 flex-wrap mb-3 align-items-center">
|
Статистика использования
|
||||||
<SavedFilters
|
</button>
|
||||||
pageKey="communities"
|
</li>
|
||||||
currentFilters={{ searchTerm, sortField, sortOrder }}
|
</ul>
|
||||||
onApplyFilter={(filters) => {
|
|
||||||
if (filters.searchTerm !== undefined) setSearchTerm(filters.searchTerm);
|
|
||||||
if (filters.sortField !== undefined) setSortField(filters.sortField);
|
|
||||||
if (filters.sortOrder !== undefined) setSortOrder(filters.sortOrder);
|
|
||||||
setCurrentPage(1);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div className="input-icon flex-grow-1" style={{ minWidth: '200px', position: 'relative' }}>
|
|
||||||
<span className="input-icon-addon"><IconSearch size={18} /></span>
|
|
||||||
<input
|
|
||||||
className="form-control"
|
|
||||||
placeholder="Поиск по value, названию, описанию и тегам"
|
|
||||||
value={searchTerm}
|
|
||||||
onChange={(e)=>{setSearchTerm(e.target.value); setCurrentPage(1);}}
|
|
||||||
style={{ paddingRight: searchTerm ? '2.5rem' : undefined }}
|
|
||||||
/>
|
|
||||||
{!!searchTerm && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-ghost-secondary btn-icon btn-sm"
|
|
||||||
style={{ position: 'absolute', right: '0.25rem', top: '50%', transform: 'translateY(-50%)', zIndex: 2 }}
|
|
||||||
aria-label="Очистить поиск"
|
|
||||||
onClick={()=>{ setSearchTerm(''); setCurrentPage(1); }}
|
|
||||||
>
|
|
||||||
<IconX size={16} />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="d-flex align-items-center gap-3 flex-wrap">
|
|
||||||
<label className="form-check form-switch mb-0 d-flex align-items-center gap-2">
|
|
||||||
<input
|
|
||||||
className="form-check-input"
|
|
||||||
type="checkbox"
|
|
||||||
checked={showOnlyUsedInFilters}
|
|
||||||
onChange={(e) => { setShowOnlyUsedInFilters(e.target.checked); setCurrentPage(1); }}
|
|
||||||
/>
|
|
||||||
<span className="form-check-label">Только используемые в фильтрах</span>
|
|
||||||
{filterUsageLoading && (
|
|
||||||
<span className="spinner-border spinner-border-sm text-primary" role="status" aria-hidden="true"></span>
|
|
||||||
)}
|
|
||||||
</label>
|
|
||||||
<button
|
|
||||||
className="btn btn-outline-secondary btn-sm"
|
|
||||||
type="button"
|
|
||||||
onClick={fetchFilterUsage}
|
|
||||||
disabled={filterUsageLoading}
|
|
||||||
title="Обновить использование из /filters"
|
|
||||||
>
|
|
||||||
<IconRefresh className="icon me-1" size={16} />
|
|
||||||
Обновить использование
|
|
||||||
</button>
|
|
||||||
{filterUsageError && (
|
|
||||||
<span className="text-danger small">{filterUsageError}</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="table-responsive">
|
|
||||||
<table className="table card-table table-vcenter table-nowrap mb-0">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th className="cursor-pointer user-select-none" onClick={()=>changeSort('value')}>
|
|
||||||
<div className="d-flex align-items-center">
|
|
||||||
<IconHash size={16} className="me-1 text-muted" />
|
|
||||||
Value
|
|
||||||
{sortField==='value' && (
|
|
||||||
<span className="ms-1">
|
|
||||||
{sortOrder==='asc' ? <IconSortAscending size={14} /> : <IconSortDescending size={14} />}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</th>
|
|
||||||
<th className="cursor-pointer user-select-none" onClick={()=>changeSort('name')}>
|
|
||||||
<div className="d-flex align-items-center">
|
|
||||||
<IconFilter size={16} className="me-1 text-muted" />
|
|
||||||
Название
|
|
||||||
{sortField==='name' && (
|
|
||||||
<span className="ms-1">
|
|
||||||
{sortOrder==='asc' ? <IconSortAscending size={14} /> : <IconSortDescending size={14} />}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</th>
|
|
||||||
<th>Описание</th>
|
|
||||||
<th>Теги</th>
|
|
||||||
<th>Gateway</th>
|
|
||||||
<th style={{ width: '10%' }}>Исп. в фильтрах</th>
|
|
||||||
<th className="text-end">Действия</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{loading ? (
|
|
||||||
<tr><td colSpan={7} className="text-center text-muted py-4">Загрузка...</td></tr>
|
|
||||||
) : paginated.length === 0 ? (
|
|
||||||
<tr><td colSpan={7} className="text-center text-muted py-4">
|
|
||||||
<EmptyState
|
|
||||||
title="Нет community"
|
|
||||||
description="Добавьте запись, чтобы начать."
|
|
||||||
action={
|
|
||||||
<button className="btn btn-primary" onClick={() => setAddModalOpen(true)}>
|
|
||||||
<IconPlus className="me-2" />
|
|
||||||
Добавить
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</td></tr>
|
|
||||||
) : paginated.map((item) => {
|
|
||||||
const usageCount = filterUsage[item.value] || 0;
|
|
||||||
return (
|
|
||||||
<tr key={item.value}>
|
|
||||||
<td>
|
|
||||||
<code className="text-blue">{item.value}</code>
|
|
||||||
{item._external && (
|
|
||||||
<span className="badge bg-yellow-lt text-yellow ms-2" title="Не внесён в справочник">не в справ.</span>
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
<td>{item.name || <span className="text-muted">—</span>}</td>
|
|
||||||
<td>
|
|
||||||
<span className="text-muted" title={item.description}>{item.description || '—'}</span>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{(item.tags || []).map(t => (
|
|
||||||
<span key={t} className="badge bg-blue-lt text-blue me-1">{t}</span>
|
|
||||||
))}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<span className="text-muted">{item.gatewayDefault || '—'}</span>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{usageCount > 0 ? (
|
|
||||||
<span className="badge bg-blue-lt text-blue">{usageCount} в фильтрах</span>
|
|
||||||
) : (
|
|
||||||
<span className="text-muted">0</span>
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
<td className="text-end">
|
|
||||||
<div className="btn-list">
|
|
||||||
<a
|
|
||||||
className="btn btn-outline-primary btn-icon btn-sm"
|
|
||||||
href={`/filters?community=${encodeURIComponent(item.value)}`}
|
|
||||||
title="Открыть в /filters с поиском по community"
|
|
||||||
>
|
|
||||||
<IconLink size={16} />
|
|
||||||
</a>
|
|
||||||
<button
|
|
||||||
className="btn btn-outline-primary btn-icon btn-sm"
|
|
||||||
onClick={()=>startEdit(item)}
|
|
||||||
title="Редактировать"
|
|
||||||
>
|
|
||||||
<IconEdit size={16} />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="btn btn-outline-danger btn-icon btn-sm"
|
|
||||||
onClick={()=>handleDeleteClick(item)}
|
|
||||||
title="Удалить"
|
|
||||||
>
|
|
||||||
<IconTrash size={16} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{totalPages > 1 && (
|
|
||||||
<Pagination
|
|
||||||
currentPage={page}
|
|
||||||
totalPages={totalPages}
|
|
||||||
totalItems={filtered.length}
|
|
||||||
pageSize={pageSize}
|
|
||||||
onPageChange={setCurrentPage}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Отображение статистики communities */}
|
|
||||||
{activeTab === 'stats' && (
|
|
||||||
<CommunityStats communities={items} />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Отображение списка communities */}
|
||||||
|
{activeTab === 'list' && (
|
||||||
|
<div className="card card-flat">
|
||||||
|
<div className="card-header">
|
||||||
|
<div className="row align-items-center w-100 g-2">
|
||||||
|
<div className="col">
|
||||||
|
<h3 className="card-title mb-0">
|
||||||
|
<IconFilter className="icon me-2" size={20} />
|
||||||
|
Список community
|
||||||
|
<span className="badge bg-blue-lt text-blue ms-2">{filtered.length}</span>
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div className="col-auto">
|
||||||
|
<button
|
||||||
|
className="btn btn-primary"
|
||||||
|
onClick={() => setAddModalOpen(true)}
|
||||||
|
>
|
||||||
|
<IconPlus className="icon me-1" />
|
||||||
|
Добавить community
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="card-body">
|
||||||
|
<div className="d-flex gap-2 flex-wrap mb-3 align-items-center">
|
||||||
|
<SavedFilters
|
||||||
|
pageKey="communities"
|
||||||
|
currentFilters={{ searchTerm, sortField, sortOrder }}
|
||||||
|
onApplyFilter={(filters) => {
|
||||||
|
if (filters.searchTerm !== undefined) setSearchTerm(filters.searchTerm);
|
||||||
|
if (filters.sortField !== undefined) setSortField(filters.sortField);
|
||||||
|
if (filters.sortOrder !== undefined) setSortOrder(filters.sortOrder);
|
||||||
|
setCurrentPage(1);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="input-icon flex-grow-1" style={{ minWidth: '200px', position: 'relative' }}>
|
||||||
|
<span className="input-icon-addon"><IconSearch size={18} /></span>
|
||||||
|
<input
|
||||||
|
className="form-control"
|
||||||
|
placeholder="Поиск по value, названию, описанию и тегам"
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e)=>{setSearchTerm(e.target.value); setCurrentPage(1);}}
|
||||||
|
style={{ paddingRight: searchTerm ? '2.5rem' : undefined }}
|
||||||
|
/>
|
||||||
|
{!!searchTerm && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-ghost-secondary btn-icon btn-sm"
|
||||||
|
style={{ position: 'absolute', right: '0.25rem', top: '50%', transform: 'translateY(-50%)', zIndex: 2 }}
|
||||||
|
aria-label="Очистить поиск"
|
||||||
|
onClick={()=>{ setSearchTerm(''); setCurrentPage(1); }}
|
||||||
|
>
|
||||||
|
<IconX size={16} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="d-flex align-items-center gap-3 flex-wrap">
|
||||||
|
<label className="form-check form-switch mb-0 d-flex align-items-center gap-2">
|
||||||
|
<input
|
||||||
|
className="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
checked={showOnlyUsedInFilters}
|
||||||
|
onChange={(e) => { setShowOnlyUsedInFilters(e.target.checked); setCurrentPage(1); }}
|
||||||
|
/>
|
||||||
|
<span className="form-check-label">Только используемые в фильтрах</span>
|
||||||
|
{filterUsageLoading && (
|
||||||
|
<span className="spinner-border spinner-border-sm text-primary" role="status" aria-hidden="true"></span>
|
||||||
|
)}
|
||||||
|
</label>
|
||||||
|
<button
|
||||||
|
className="btn btn-outline-secondary btn-sm"
|
||||||
|
type="button"
|
||||||
|
onClick={fetchFilterUsage}
|
||||||
|
disabled={filterUsageLoading}
|
||||||
|
title="Обновить использование из /filters"
|
||||||
|
>
|
||||||
|
<IconRefresh className="icon me-1" size={16} />
|
||||||
|
Обновить использование
|
||||||
|
</button>
|
||||||
|
{filterUsageError && (
|
||||||
|
<span className="text-danger small">{filterUsageError}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="table-responsive">
|
||||||
|
<table className="table card-table table-vcenter table-nowrap mb-0">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th className="cursor-pointer user-select-none" onClick={()=>changeSort('value')}>
|
||||||
|
<div className="d-flex align-items-center">
|
||||||
|
<IconHash size={16} className="me-1 text-muted" />
|
||||||
|
Value
|
||||||
|
{sortField==='value' && (
|
||||||
|
<span className="ms-1">
|
||||||
|
{sortOrder==='asc' ? <IconSortAscending size={14} /> : <IconSortDescending size={14} />}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
<th className="cursor-pointer user-select-none" onClick={()=>changeSort('name')}>
|
||||||
|
<div className="d-flex align-items-center">
|
||||||
|
<IconFilter size={16} className="me-1 text-muted" />
|
||||||
|
Название
|
||||||
|
{sortField==='name' && (
|
||||||
|
<span className="ms-1">
|
||||||
|
{sortOrder==='asc' ? <IconSortAscending size={14} /> : <IconSortDescending size={14} />}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
<th>Описание</th>
|
||||||
|
<th>Теги</th>
|
||||||
|
<th>Gateway</th>
|
||||||
|
<th style={{ width: '10%' }}>Исп. в фильтрах</th>
|
||||||
|
<th className="text-end">Действия</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{loading ? (
|
||||||
|
<tr><td colSpan={7} className="text-center text-muted py-4">Загрузка...</td></tr>
|
||||||
|
) : paginated.length === 0 ? (
|
||||||
|
<tr><td colSpan={7} className="text-center text-muted py-4">
|
||||||
|
<EmptyState
|
||||||
|
title="Нет community"
|
||||||
|
description="Добавьте запись, чтобы начать."
|
||||||
|
action={
|
||||||
|
<button className="btn btn-primary" onClick={() => setAddModalOpen(true)}>
|
||||||
|
<IconPlus className="me-2" />
|
||||||
|
Добавить
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</td></tr>
|
||||||
|
) : paginated.map((item) => {
|
||||||
|
const usageCount = filterUsage[item.value] || 0;
|
||||||
|
return (
|
||||||
|
<tr key={item.value}>
|
||||||
|
<td>
|
||||||
|
<code className="text-blue">{item.value}</code>
|
||||||
|
{item._external && (
|
||||||
|
<span className="badge bg-yellow-lt text-yellow ms-2" title="Не внесён в справочник">не в справ.</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td>{item.name || <span className="text-muted">—</span>}</td>
|
||||||
|
<td>
|
||||||
|
<span className="text-muted" title={item.description}>{item.description || '—'}</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{(item.tags || []).map(t => (
|
||||||
|
<span key={t} className="badge bg-blue-lt text-blue me-1">{t}</span>
|
||||||
|
))}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span className="text-muted">{item.gatewayDefault || '—'}</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{usageCount > 0 ? (
|
||||||
|
<span className="badge bg-blue-lt text-blue">{usageCount} в фильтрах</span>
|
||||||
|
) : (
|
||||||
|
<span className="text-muted">0</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td className="text-end">
|
||||||
|
<div className="btn-list">
|
||||||
|
<a
|
||||||
|
className="btn btn-outline-primary btn-icon btn-sm"
|
||||||
|
href={`/filters?community=${encodeURIComponent(item.value)}`}
|
||||||
|
title="Открыть в /filters с поиском по community"
|
||||||
|
>
|
||||||
|
<IconLink size={16} />
|
||||||
|
</a>
|
||||||
|
<button
|
||||||
|
className="btn btn-outline-primary btn-icon btn-sm"
|
||||||
|
onClick={()=>startEdit(item)}
|
||||||
|
title="Редактировать"
|
||||||
|
>
|
||||||
|
<IconEdit size={16} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn btn-outline-danger btn-icon btn-sm"
|
||||||
|
onClick={()=>handleDeleteClick(item)}
|
||||||
|
title="Удалить"
|
||||||
|
>
|
||||||
|
<IconTrash size={16} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{totalPages > 1 && (
|
||||||
|
<Pagination
|
||||||
|
currentPage={page}
|
||||||
|
totalPages={totalPages}
|
||||||
|
totalItems={filtered.length}
|
||||||
|
pageSize={pageSize}
|
||||||
|
onPageChange={setCurrentPage}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Отображение статистики communities */}
|
||||||
|
{activeTab === 'stats' && (
|
||||||
|
<CommunityStats communities={items} />
|
||||||
|
)}
|
||||||
{/* Модальное окно добавления */}
|
{/* Модальное окно добавления */}
|
||||||
<FormModal
|
<FormModal
|
||||||
show={addModalOpen}
|
show={addModalOpen}
|
||||||
|
|||||||
Reference in New Issue
Block a user