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 && (
)}
- {/* Поиск */}
-
+
- {/* Скроллируемый контент */}
-
- {/* Секция: Страны */}
-
-
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}