feat: Group communities by tags in EasySwitchManager for improved organization and visibility, enhancing user experience with clear categorization and display of communities without tags.
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m58s

This commit is contained in:
2025-12-07 19:46:09 +07:00
parent 37b34f718c
commit 260985573c
+192 -10
View File
@@ -622,15 +622,57 @@ function EasySwitchManager() {
{/* Контент (сворачиваемый) */}
{isExpanded && (
<div className="card-body p-3">
{/* Сетка Communities для этого сервера */}
<div className="row g-3">
{filteredCommunitiesForServer.map((comm) => {
const activeKey = `${server.id}:${comm.value}`;
const activeGw = activeGateways[activeKey];
const hasFilterForComm = server.filtersMap.has(comm.value);
{/* Группируем communities по тегам */}
{(() => {
// Группируем по тегам
const groupedByTags = new Map();
const noTagCommunities = [];
filteredCommunitiesForServer.forEach(comm => {
const tags = Array.isArray(comm.tags) ? comm.tags :
(comm.tags ? String(comm.tags).split(',').map(t => t.trim()).filter(Boolean) : []);
return (
<div key={comm.value} className="col-12 col-md-6 col-lg-4">
if (tags.length === 0) {
noTagCommunities.push(comm);
} else {
tags.forEach(tag => {
if (!groupedByTags.has(tag)) {
groupedByTags.set(tag, []);
}
groupedByTags.get(tag).push(comm);
});
}
});
// Сортируем теги
const sortedTags = Array.from(groupedByTags.keys()).sort();
return (
<>
{/* Communities с тегами */}
{sortedTags.map(tag => {
const tagCommunities = groupedByTags.get(tag);
return (
<div key={tag} className="mb-4">
{/* Заголовок группы тегов */}
<div className="mb-2 pb-2 border-bottom">
<span className="badge bg-purple-lt text-purple me-2" style={{ fontSize: '0.85rem', padding: '6px 10px' }}>
{tag}
</span>
<span className="text-muted small">
{tagCommunities.length} {tagCommunities.length === 1 ? 'community' : 'communities'}
</span>
</div>
{/* Сетка Communities для этого тега */}
<div className="row g-3">
{tagCommunities.map((comm) => {
const activeKey = `${server.id}:${comm.value}`;
const activeGw = activeGateways[activeKey];
const hasFilterForComm = server.filtersMap.has(comm.value);
return (
<div key={comm.value} className="col-12 col-md-6 col-lg-4">
<div className="mb-2">
{/* Заголовок community */}
<div className="mb-2">
@@ -738,8 +780,148 @@ function EasySwitchManager() {
</div>
</div>
);
})}
</div>
})}
</div>
</div>
);
})}
{/* Communities без тегов */}
{noTagCommunities.length > 0 && (
<div className="mb-4">
{/* Заголовок группы без тегов */}
<div className="mb-2 pb-2 border-bottom">
<span className="badge bg-gray-lt text-gray me-2" style={{ fontSize: '0.85rem', padding: '6px 10px' }}>
Без тегов
</span>
<span className="text-muted small">
{noTagCommunities.length} {noTagCommunities.length === 1 ? 'community' : 'communities'}
</span>
</div>
{/* Сетка Communities без тегов */}
<div className="row g-3">
{noTagCommunities.map((comm) => {
const activeKey = `${server.id}:${comm.value}`;
const activeGw = activeGateways[activeKey];
const hasFilterForComm = server.filtersMap.has(comm.value);
return (
<div key={comm.value} className="col-12 col-md-6 col-lg-4">
<div className="mb-2">
{/* Заголовок community */}
<div className="mb-2">
<span className="badge bg-blue-lt text-blue me-2" style={{ fontSize: '0.8rem' }}>
{comm.value}
</span>
<span className="fw-medium">{comm.name || comm.description || ''}</span>
{hasFilterForComm && (
<span className="badge bg-green-lt text-green ms-2" style={{ fontSize: '0.7rem' }}>
настроен
</span>
)}
<span className="text-muted small ms-2">
({server.gateways.length})
</span>
</div>
{/* Сетка карточек шлюзов для этого community */}
<div className="row g-2">
{server.gateways.map((gw, idx) => {
const isActive = activeGw === gw.name;
const isFastest = idx === 0;
const ping = 20 + idx * 10 + Math.floor(Math.random() * 15);
return (
<div key={gw.name} className="col-12 col-sm-4">
<div
className={`card cursor-pointer h-100 ${isActive ? 'bg-primary text-white' : ''}`}
style={{
cursor: 'pointer',
transition: 'all 0.15s ease',
border: isActive ? '2px solid var(--tblr-primary)' : '1px solid rgba(0,0,0,0.1)',
borderRadius: '6px',
boxShadow: isActive ? '0 3px 8px rgba(32, 107, 196, 0.3)' : 'none',
position: 'relative'
}}
onClick={() => handleGatewaySelect(server.id, comm.value, gw.name)}
onMouseEnter={(e) => {
if (!isActive) {
e.currentTarget.style.boxShadow = '0 2px 6px rgba(0,0,0,0.1)';
e.currentTarget.style.transform = 'translateY(-1px)';
}
}}
onMouseLeave={(e) => {
if (!isActive) {
e.currentTarget.style.boxShadow = 'none';
e.currentTarget.style.transform = 'translateY(0)';
}
}}
>
<div className="card-body p-2">
{/* Галочка (абсолютное позиционирование) */}
{isActive && (
<div
className="bg-white text-primary rounded-circle d-flex align-items-center justify-content-center"
style={{
width: '20px',
height: '20px',
position: 'absolute',
top: '8px',
right: '8px'
}}
>
<IconCheck size={12} />
</div>
)}
{/* Метка Fastest */}
<div className="mb-1" style={{ minHeight: '18px' }}>
{isFastest && !isActive && (
<span className="badge bg-yellow-lt text-yellow d-inline-flex align-items-center" style={{ fontSize: '0.65rem', padding: '2px 6px' }}>
<IconBolt size={10} className="me-1" />
Fast
</span>
)}
</div>
{/* Название шлюза */}
<div className={`fw-bold mb-1 ${isActive ? 'text-white' : ''}`} style={{ fontSize: '0.85rem', lineHeight: '1.2' }}>
{gw.name}
</div>
{/* Описание шлюза */}
{gw.comment && (
<div className={`small mb-1 ${isActive ? 'text-white' : 'text-muted'}`} style={{ fontSize: '0.7rem', opacity: isActive ? 0.8 : 1, lineHeight: '1.2' }}>
{gw.comment}
</div>
)}
{/* Протокол */}
<div className={`small mb-1 ${isActive ? 'text-white' : 'text-muted'}`} style={{ fontSize: '0.7rem', opacity: isActive ? 0.85 : 1 }}>
{serverMeta?.provider || 'vless'}
</div>
{/* Метрика (пинг) */}
<div className={`fw-bold mt-1 ${isActive ? 'text-white' : 'text-success'}`} style={{ fontSize: '1.1rem' }}>
{ping}
</div>
</div>
</div>
</div>
);
})}
</div>
</div>
</div>
);
})}
</div>
</div>
)}
</>
);
})()}
</div>
)}
</div>