From 3a7578ce2624b0524d1dafe12d60a031af296e99 Mon Sep 17 00:00:00 2001 From: shats Date: Mon, 16 Feb 2026 16:03:28 +0700 Subject: [PATCH] refactor(ServerSidebar): implement new filter section and sidebar styling for improved user interaction and organization --- frontend/src/App.css | 36 ++ .../src/components/server/ServerSidebar.jsx | 493 ++++++------------ 2 files changed, 203 insertions(+), 326 deletions(-) diff --git a/frontend/src/App.css b/frontend/src/App.css index 66c196e..ccea005 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -256,6 +256,42 @@ /* Убрали глобальную перекраску .badge, чтобы работали текстовые цвета Tabler (например, badge bg-blue-lt text-blue) */ +/* --- Server sidebar (фильтры на /servers) — Tabler card + list-group --- */ +.server-sidebar { + width: 280px; + min-width: 280px; + max-height: calc(100vh - 180px); + display: flex; + flex-direction: column; +} +.server-sidebar .card-header { + flex-shrink: 0; +} +.server-sidebar .card-body.border-bottom, +.server-sidebar .card-footer { + flex-shrink: 0; +} +.server-sidebar-body { + flex: 1; + min-height: 0; + max-height: 50vh; +} +.server-sidebar .filter-section .list-group-header { + cursor: pointer; + font-size: 0.8125rem; + font-weight: 600; + text-transform: none; + color: var(--tblr-body-color); +} +.server-sidebar .filter-section .list-group-header:hover { + background-color: var(--tblr-bg-surface-tertiary); +} +.server-sidebar .list-group-item { + font-size: 0.8125rem; + padding-top: 0.375rem; + padding-bottom: 0.375rem; +} + /* Responsive navbar */ @media (max-width: 991.98px) { .navbar-collapse { diff --git a/frontend/src/components/server/ServerSidebar.jsx b/frontend/src/components/server/ServerSidebar.jsx index edcc282..01f6037 100644 --- a/frontend/src/components/server/ServerSidebar.jsx +++ b/frontend/src/components/server/ServerSidebar.jsx @@ -6,29 +6,63 @@ import { IconNetwork, IconChevronDown, IconChevronRight, - IconX, - IconSearch, IconFilter, - IconDatabase + IconSearch, + IconDatabase, + IconX } from '@tabler/icons-react'; -// Преобразование кода страны в emoji-флаг function countryToFlag(isoCode) { if (!isoCode) return ''; const codeMap = { - 'SWE': 'SE', - 'RUS': 'RU', - 'USA': 'US', - 'GER': 'DE', - 'FIN': 'FI', - 'NLD': 'NL' + SWE: 'SE', + RUS: 'RU', + USA: 'US', + GER: 'DE', + FIN: 'FI', + NLD: 'NL' }; const code = codeMap[isoCode?.toUpperCase()] || isoCode?.toUpperCase(); if (!code || code.length !== 2) return isoCode; - return code - .replace(/./g, char => - String.fromCodePoint(127397 + char.charCodeAt()) - ); + return code.replace(/./g, char => String.fromCodePoint(127397 + char.charCodeAt())); +} + +function FilterSection({ title, icon: Icon, iconColor, expanded, onToggle, children }) { + return ( +
+ + {expanded &&
{children}
} +
+ ); +} + +function FilterItem({ label, count, active, onClick, leftAddon }) { + return ( + + ); } export default function ServerSidebar({ @@ -46,378 +80,185 @@ export default function ServerSidebar({ tunnel: false }); - // Подсчёт серверов по категориям const stats = useMemo(() => { const byCountry = {}; const byProvider = {}; const byType = {}; const byTunnel = {}; - servers.forEach(srv => { byCountry[srv.country] = (byCountry[srv.country] || 0) + 1; byProvider[srv.provider] = (byProvider[srv.provider] || 0) + 1; byType[srv.type] = (byType[srv.type] || 0) + 1; byTunnel[srv.tunnel] = (byTunnel[srv.tunnel] || 0) + 1; }); - return { byCountry, byProvider, byType, byTunnel }; }, [servers]); - const toggleSection = (section) => { - setExpandedSections(prev => ({ - ...prev, - [section]: !prev[section] - })); + const toggleSection = section => { + setExpandedSections(prev => ({ ...prev, [section]: !prev[section] })); }; const handleFilterClick = (category, value) => { - const currentValue = filters[category]; - const newValue = currentValue === value ? '' : value; - onFiltersChange({ ...filters, [category]: newValue }); + const current = filters[category]; + onFiltersChange({ ...filters, [category]: current === value ? '' : value }); }; const activeFiltersCount = Object.values(filters).filter(Boolean).length + (searchTerm ? 1 : 0); - - const typeLabels = { - 'jumphost': 'Jumphost', - 'exit': 'Выходная нода' - }; + const typeLabels = { jumphost: 'Jumphost', exit: 'Выходная нода' }; return ( -
- {/* Заголовок */} -
+
+
Фильтры {activeFiltersCount > 0 && ( - - {activeFiltersCount} - + {activeFiltersCount} )}
{activeFiltersCount > 0 && ( )}
- {/* Поиск */} -
+
- + onSearch(e.target.value)} - style={{ - background: 'rgba(0,0,0,0.02)', - border: '1px solid rgba(0,0,0,0.06)', - borderRadius: '8px', - fontSize: '0.85rem' - }} + onChange={e => onSearch(e.target.value)} + aria-label="Поиск серверов" />
- {/* Скроллируемый контент */} -
- {/* Секция: Страны */} -
-
toggleSection('country')} - style={{ - background: 'rgba(0,0,0,0.02)', - borderBottom: '1px solid rgba(0,0,0,0.04)' - }} - > -
- - Страны -
- {expandedSections.country ? : } -
- {expandedSections.country && ( -
- {Object.entries(stats.byCountry) - .sort((a, b) => b[1] - a[1]) - .map(([country, count]) => ( - - ))} -
- )} -
+
+ toggleSection('country')} + > + {Object.entries(stats.byCountry) + .sort((a, b) => b[1] - a[1]) + .map(([country, count]) => ( + handleFilterClick('country', country)} + leftAddon={ + + {countryToFlag(country)} + + } + /> + ))} + - {/* Секция: Провайдеры */} -
-
toggleSection('provider')} - style={{ - background: 'rgba(0,0,0,0.02)', - borderBottom: '1px solid rgba(0,0,0,0.04)' - }} - > -
- - Провайдеры -
- {expandedSections.provider ? : } -
- {expandedSections.provider && ( -
- {Object.entries(stats.byProvider) - .sort((a, b) => b[1] - a[1]) - .map(([provider, count]) => ( - - ))} -
- )} -
+ toggleSection('provider')} + > + {Object.entries(stats.byProvider) + .sort((a, b) => b[1] - a[1]) + .map(([provider, count]) => ( + handleFilterClick('provider', provider)} + /> + ))} + - {/* Секция: Типы */} -
-
toggleSection('type')} - style={{ - background: 'rgba(0,0,0,0.02)', - borderBottom: '1px solid rgba(0,0,0,0.04)' - }} - > -
- - Тип сервера -
- {expandedSections.type ? : } -
- {expandedSections.type && ( -
- {Object.entries(stats.byType) - .sort((a, b) => b[1] - a[1]) - .map(([type, count]) => ( - - ))} -
- )} -
+ aria-hidden + /> + } + /> + ))} + - {/* Секция: Туннели */} -
-
toggleSection('tunnel')} - style={{ - background: 'rgba(0,0,0,0.02)', - borderBottom: '1px solid rgba(0,0,0,0.04)' - }} - > -
- - Туннель -
- {expandedSections.tunnel ? : } -
- {expandedSections.tunnel && ( -
- {Object.entries(stats.byTunnel) - .sort((a, b) => b[1] - a[1]) - .map(([tunnel, count]) => ( - - ))} -
- )} -
+ toggleSection('tunnel')} + > + {Object.entries(stats.byTunnel) + .sort((a, b) => b[1] - a[1]) + .map(([tunnel, count]) => ( + handleFilterClick('tunnel', tunnel)} + /> + ))} +
- {/* Статистика внизу */} -
+
- - Статистика + + Статистика
-
- Всего серверов - {servers.length} +
+ Всего серверов + {servers.length}
-
- Выходных нод - {stats.byType['exit'] || 0} +
+ Выходных нод + {stats.byType['exit'] || 0}
-
- Jumphosts - {stats.byType['jumphost'] || 0} +
+ Jumphosts + {stats.byType['jumphost'] || 0}