diff --git a/backend/routes/communitiesRoutes.js b/backend/routes/communitiesRoutes.js index 2c13c56..f5793a6 100644 --- a/backend/routes/communitiesRoutes.js +++ b/backend/routes/communitiesRoutes.js @@ -24,7 +24,8 @@ async function getCommunities(req, res) { description: c.description ? String(c.description) : '', tags: Array.isArray(c.tags) ? c.tags.map(String) : [], gatewayDefault: c.gatewayDefault ? String(c.gatewayDefault) : '', - color: c.color ? String(c.color) : '' + color: c.color ? String(c.color) : '', + icon: c.icon ? String(c.icon) : '' })); }, defaultValue: [] @@ -70,6 +71,7 @@ async function postCommunities(req, res) { tags: Array.isArray(entry.tags) ? entry.tags.map(String) : [], gatewayDefault: entry.gatewayDefault ? String(entry.gatewayDefault) : '', color: entry.color ? String(entry.color) : '', + icon: entry.icon ? String(entry.icon) : '', category: entry.category ? String(entry.category) : '', priority: typeof entry.priority === 'number' ? entry.priority : 0, enabled: typeof entry.enabled === 'boolean' ? entry.enabled : true, diff --git a/frontend/src/CommunitiesManager.jsx b/frontend/src/CommunitiesManager.jsx index f853965..581f5d6 100644 --- a/frontend/src/CommunitiesManager.jsx +++ b/frontend/src/CommunitiesManager.jsx @@ -11,7 +11,6 @@ import { IconFilter, IconChartBar, IconList, - IconLink, IconRefresh, IconSortAscending, IconSortDescending, @@ -29,6 +28,8 @@ import ConfirmModal from './components/ConfirmModal.jsx'; import FormField from './components/FormField.jsx'; import ErrorAlert from './components/ErrorAlert.jsx'; import Pagination from './components/Pagination.jsx'; +import IconPicker from './components/IconPicker.jsx'; +import { getIconById } from './lib/brandIcons.js'; const API_URL = '/api'; @@ -53,9 +54,9 @@ function CommunitiesManager() { // Состояние для переключения между списком и статистикой const [activeTab, setActiveTab] = useState('list'); // 'list' | 'stats' - const [newItem, setNewItem] = useState({ value: '', name: '', description: '', tags: '', gatewayDefault: '', color: '', category: '', priority: 0, enabled: true }); + const [newItem, setNewItem] = useState({ value: '', name: '', description: '', tags: '', gatewayDefault: '', color: '', icon: '', category: '', priority: 0, enabled: true }); const [editingValue, setEditingValue] = useState(null); - const [editingDraft, setEditingDraft] = useState({ value: '', name: '', description: '', tags: '', gatewayDefault: '', color: '', category: '', priority: 0, enabled: true }); + const [editingDraft, setEditingDraft] = useState({ value: '', name: '', description: '', tags: '', gatewayDefault: '', color: '', icon: '', category: '', priority: 0, enabled: true }); const [filterUsage, setFilterUsage] = useState({}); const [filterUsageLoading, setFilterUsageLoading] = useState(false); const [filterUsageError, setFilterUsageError] = useState(''); @@ -79,7 +80,7 @@ function CommunitiesManager() { Object.keys(filterUsage).forEach((value) => { if (!value) return; if (!map.has(value)) { - map.set(value, { value, name: '', description: '', tags: [], gatewayDefault: '', color: '', _external: true }); + map.set(value, { value, name: '', description: '', tags: [], gatewayDefault: '', color: '', icon: '', _external: true }); } }); return Array.from(map.values()); @@ -130,13 +131,14 @@ function CommunitiesManager() { tags: Array.isArray(d.tags) ? d.tags : [], gatewayDefault: String(d.gatewayDefault || ''), color: String(d.color || ''), + icon: String(d.icon || ''), _external: false, }); } // add unknown ones from usage for (const v of usedCommunities) { if (!map.has(v)) { - map.set(v, { value: v, name: '', description: '', tags: [], gatewayDefault: '', color: '', _external: true }); + map.set(v, { value: v, name: '', description: '', tags: [], gatewayDefault: '', color: '', icon: '', _external: true }); } } setItems(Array.from(map.values())); @@ -217,8 +219,9 @@ function CommunitiesManager() { tags: toTagsArray(newItem.tags), gatewayDefault: String(newItem.gatewayDefault || ''), color: String(newItem.color || ''), + icon: String(newItem.icon || ''), }]); - setNewItem({ value: '', name: '', description: '', tags: '', gatewayDefault: '', color: '' }); + setNewItem({ value: '', name: '', description: '', tags: '', gatewayDefault: '', color: '', icon: '' }); setAddModalOpen(false); setSuccess('Community успешно добавлен!'); setTimeout(() => setSuccess(''), 3000); @@ -233,6 +236,7 @@ function CommunitiesManager() { tags: fromTagsArray(item.tags), gatewayDefault: item.gatewayDefault || '', color: item.color || '', + icon: item.icon || '', }); setEditModalOpen(true); }; @@ -249,6 +253,7 @@ function CommunitiesManager() { tags: toTagsArray(editingDraft.tags), gatewayDefault: String(editingDraft.gatewayDefault || ''), color: String(editingDraft.color || ''), + icon: String(editingDraft.icon || ''), } : i); setItems(next); setEditingValue(null); @@ -278,7 +283,7 @@ function CommunitiesManager() { }; const handleImport = () => { - const text = window.prompt('Вставьте JSON-массив community (value, name, description, tags, gatewayDefault, color)'); + const text = window.prompt('Вставьте JSON-массив community (value, name, description, tags, gatewayDefault, color, icon)'); if (!text) return; try { const arr = JSON.parse(text); @@ -297,6 +302,7 @@ function CommunitiesManager() { tags: Array.isArray(x.tags) ? x.tags : [], gatewayDefault: String(x.gatewayDefault || ''), color: String(x.color || ''), + icon: String(x.icon || ''), }); }); return Array.from(map.values()); @@ -553,6 +559,7 @@ function CommunitiesManager() { + {loading ? ( - + ) : paginated.length === 0 ? ( - ) : paginated.map((item) => { const usageCount = filterUsage[item.value] || 0; + const iconInfo = item.icon ? getIconById(item.icon) : null; return ( +
Иконка changeSort('value')}>
@@ -584,9 +591,9 @@ function CommunitiesManager() {
Загрузка...
Загрузка...
+
+ {iconInfo ? ( + + + + ) : ( + + )} + {item.value} {item._external && ( @@ -629,13 +646,6 @@ function CommunitiesManager() {
- - - {open && ( -
+
@@ -72,7 +81,7 @@ export default function IconPicker({ value, onChange, disabled = false, label =
{filtered.map((item) => ( @@ -82,7 +91,7 @@ export default function IconPicker({ value, onChange, disabled = false, label = className={`btn btn-sm d-flex align-items-center justify-content-center rounded ${ value === item.id ? 'btn-primary' : 'btn-outline-secondary' }`} - style={{ width: 40, height: 40 }} + style={{ width: 48, height: 48 }} onClick={() => handleSelect(item)} title={item.label} role="option" diff --git a/frontend/src/lib/brandIcons.js b/frontend/src/lib/brandIcons.js index ba498fa..474345b 100644 --- a/frontend/src/lib/brandIcons.js +++ b/frontend/src/lib/brandIcons.js @@ -37,11 +37,14 @@ import { IconBrandVisa, IconBrandMastercard, IconWorld, + IconCloud, } from '@tabler/icons-react'; const BRAND_ICONS = [ { id: 'IconBrandGoogle', label: 'Google', Icon: IconBrandGoogle }, { id: 'IconBrandCloudflare', label: 'Cloudflare', Icon: IconBrandCloudflare }, + { id: 'IconCdnBunny', label: 'Bunny CDN', Icon: IconCloud }, + { id: 'IconCdnFastly', label: 'Fastly CDN', Icon: IconCloud }, { id: 'IconBrandYandex', label: 'Yandex', Icon: IconBrandYandex }, { id: 'IconBrandInstagram', label: 'Instagram', Icon: IconBrandInstagram }, { id: 'IconBrandGithub', label: 'GitHub', Icon: IconBrandGithub }, diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index 5bf6b52..57b92ea 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -58,6 +58,7 @@ export interface Community { name?: string; description?: string; gatewayDefault?: string; + icon?: string; category?: string; }