diff --git a/backend/routes/miscRoutes.js b/backend/routes/miscRoutes.js index 391fd05..96baeee 100644 --- a/backend/routes/miscRoutes.js +++ b/backend/routes/miscRoutes.js @@ -331,14 +331,29 @@ function measureTcpRtt(host, port = 443, timeoutMs = 6000) { }); } -/** Список целей для пинга с главной: id, имя, хост, порт */ -const PING_SERVICES = [ - { id: 'google', name: 'Google', host: '8.8.8.8', port: 443 }, - { id: 'cloudflare', name: 'Cloudflare', host: '1.1.1.1', port: 443 }, - { id: 'yandex', name: 'Yandex', host: 'ya.ru', port: 443 }, - { id: 'instagram', name: 'Instagram', host: 'instagram.com', port: 443 }, +/** Список целей для пинга по умолчанию (id, name, host, port, icon, color) */ +const PING_SERVICES_DEFAULT = [ + { id: 'google', name: 'Google', host: '8.8.8.8', port: 443, icon: 'IconBrandGoogle', color: 'blue' }, + { id: 'cloudflare', name: 'Cloudflare', host: '1.1.1.1', port: 443, icon: 'IconBrandCloudflare', color: 'orange' }, + { id: 'yandex', name: 'Yandex', host: 'ya.ru', port: 443, icon: 'IconBrandYandex', color: 'red' }, + { id: 'instagram', name: 'Instagram', host: 'instagram.com', port: 443, icon: 'IconBrandInstagram', color: 'pink' }, ]; +function getPingServicesListFromSettings(uiSettings) { + const raw = uiSettings.pingServicesList; + if (Array.isArray(raw) && raw.length > 0) { + return raw.map((s) => ({ + id: String(s?.id ?? '').trim() || `svc-${Math.random().toString(36).slice(2, 9)}`, + name: String(s?.name ?? '').trim() || 'Сервис', + host: String(s?.host ?? '').trim() || '0.0.0.0', + port: Math.max(1, parseInt(s?.port, 10) || 443), + icon: typeof s?.icon === 'string' ? s.icon : 'IconWorld', + color: typeof s?.color === 'string' ? s.color : 'primary', + })).filter((s) => s.host !== '0.0.0.0'); + } + return PING_SERVICES_DEFAULT; +} + /** Загрузить UI-настройки из S3 (для pingServicesSource и др.) */ async function loadUiSettingsSync() { try { @@ -353,16 +368,26 @@ async function loadUiSettingsSync() { const PING_SERVICES_CACHE_KEY_PREFIX = 'ping-services/cache_'; -// GET /api/ping-services — RTT до Google, Cloudflare, Yandex, Instagram (веб или через RouterOS по настройке) -async function getPingServices(req, res) { - const fallbackPayload = { - google: { id: 'google', name: 'Google', host: '8.8.8.8', ms: null }, - cloudflare: { id: 'cloudflare', name: 'Cloudflare', host: '1.1.1.1', ms: null }, - yandex: { id: 'yandex', name: 'Yandex', host: 'ya.ru', ms: null }, - instagram: { id: 'instagram', name: 'Instagram', host: 'instagram.com', ms: null }, - }; +// GET /api/ping-services-list — список сервисов для пинга из настроек (для дашборда и редактора) +async function getPingServicesList(req, res) { try { const uiSettings = await loadUiSettingsSync(); + const list = getPingServicesListFromSettings(uiSettings); + return res.json({ list }); + } catch (e) { + console.error('ping-services-list error', e); + return res.json({ list: PING_SERVICES_DEFAULT }); + } +} + +// GET /api/ping-services — RTT до сервисов из списка (веб или через RouterOS по настройке) +async function getPingServices(req, res) { + try { + const uiSettings = await loadUiSettingsSync(); + const servicesList = getPingServicesListFromSettings(uiSettings); + const fallbackPayload = {}; + servicesList.forEach((s) => { fallbackPayload[s.id] = { id: s.id, name: s.name, host: s.host, ms: null }; }); + const viaRouter = String(uiSettings.pingServicesSource || 'web').toLowerCase() === 'router'; const cacheSeconds = Math.max(0, parseInt(uiSettings.pingServicesCacheSeconds, 10) || 0); @@ -414,7 +439,7 @@ async function getPingServices(req, res) { return res.json(fallbackPayload); } const results = await Promise.all( - PING_SERVICES.map(async (svc) => { + servicesList.map(async (svc) => { try { const result = await runPingViaRouter(serverId, gatewayIpResolved || null, svc.host, 3); const ms = typeof result.avgMs === 'number' ? Math.round(result.avgMs) : null; @@ -434,7 +459,7 @@ async function getPingServices(req, res) { } const results = await Promise.all( - PING_SERVICES.map(async (svc) => { + servicesList.map(async (svc) => { const ms = await measureTcpRtt(svc.host, svc.port, 6000); return { id: svc.id, name: svc.name, host: svc.host, ms }; }) @@ -447,7 +472,9 @@ async function getPingServices(req, res) { res.json(byId); } catch (e) { console.error('ping-services error', e); - res.status(500).json(fallbackPayload); + const fallback = {}; + PING_SERVICES_DEFAULT.forEach((s) => { fallback[s.id] = { id: s.id, name: s.name, host: s.host, ms: null }; }); + res.status(500).json(fallback); } } @@ -666,6 +693,7 @@ module.exports = { getWsUrl, getUiSettings, postUiSettings, + getPingServicesList, getPingServices, }; diff --git a/backend/server.js b/backend/server.js index f119fc3..f16f629 100644 --- a/backend/server.js +++ b/backend/server.js @@ -439,6 +439,7 @@ app.post('/api/update-bgp/background', bgpUpdateLimiter, miscRoutes.updateBgpBac app.get('/api/ws/url', miscRoutes.getWsUrl); app.get('/api/ui-settings', miscRoutes.getUiSettings); app.post('/api/ui-settings', miscRoutes.postUiSettings); +app.get('/api/ping-services-list', miscRoutes.getPingServicesList); app.get('/api/ping-services', miscRoutes.getPingServices); // === IPSEC PASSWORDS === diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 3643388..7b14c42 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -37,6 +37,7 @@ import NetworkConfigManager from './NetworkConfigManager'; import MikrotikTools from './MikrotikTools.jsx'; import Dashboard from './Dashboard'; import MikrotikBackupsManager from './MikrotikBackupsManager.jsx'; +import PingServicesManager from './PingServicesManager.jsx'; import './App.css'; import { NotifyProvider } from './components/NotifyProvider.jsx'; import SettingsModal from './components/SettingsModal.jsx'; @@ -57,7 +58,7 @@ function LanguageProvider({ children }) { home: 'Главная', data: 'Данные', management: 'Управление', tools: 'Инструменты', dashboard: 'Панель', domains: 'Домены', ipRanges: 'IP-диапазоны', asns: 'AS', communities: 'Community', servers: 'Серверы', filters: 'Фильтры', billing: 'Биллинг', autoUrls: 'Авто URL', - easySwitch: 'Easy Switch', networkConfig: 'Сетевые настройки', mikrotikBackups: 'MikroTik Бэкапы', + easySwitch: 'Easy Switch', networkConfig: 'Сетевые настройки', mikrotikBackups: 'MikroTik Бэкапы', pingServices: 'Пинг сервисов', light: 'Светлая', dark: 'Тёмная', layoutSidebar: 'Сайдбар', layoutHorizontal: 'Верхнее меню' }, @@ -65,7 +66,7 @@ function LanguageProvider({ children }) { home: 'Home', data: 'Data', management: 'Management', tools: 'Tools', dashboard: 'Dashboard', domains: 'Domains', ipRanges: 'IP Ranges', asns: 'ASNs', communities: 'Communities', servers: 'Servers', filters: 'Filters', billing: 'Billing', autoUrls: 'Auto URLs', - easySwitch: 'Easy Switch', networkConfig: 'Network Config', mikrotikBackups: 'MikroTik Backups', + easySwitch: 'Easy Switch', networkConfig: 'Network Config', mikrotikBackups: 'MikroTik Backups', pingServices: 'Ping Services', light: 'Light', dark: 'Dark', layoutSidebar: 'Sidebar', layoutHorizontal: 'Top menu' } @@ -221,7 +222,8 @@ function MainLayout() { items: [ { id: 'auto-urls', title: t('autoUrls'), path: '/auto-urls', icon: IconDownload }, { id: 'mikrotik-backups', title: t('mikrotikBackups'), path: '/mikrotik-backups', icon: IconDatabase }, - { id: 'mikrotik-tools', title: 'MikroTik Инструменты', path: '/mikrotik-tools', icon: IconNetwork } + { id: 'mikrotik-tools', title: 'MikroTik Инструменты', path: '/mikrotik-tools', icon: IconNetwork }, + { id: 'ping-services', title: t('pingServices'), path: '/ping-services', icon: IconNetwork } ] }, // Убрали неиспользуемые/неработающие разделы @@ -380,6 +382,7 @@ function MainLayout() { } /> } /> } /> + } /> } /> @@ -535,6 +538,7 @@ function MainLayout() { } /> } /> } /> + } /> } /> diff --git a/frontend/src/Dashboard.jsx b/frontend/src/Dashboard.jsx index 31b279b..0b5c534 100644 --- a/frontend/src/Dashboard.jsx +++ b/frontend/src/Dashboard.jsx @@ -16,11 +16,8 @@ import { IconDownload, IconCreditCard, IconSearch, - IconBrandGoogle, - IconBrandCloudflare, - IconBrandYandex, - IconBrandInstagram } from '@tabler/icons-react'; +import { getIconById } from './lib/brandIcons.js'; import PageHeader from './components/PageHeader.jsx'; import TopNStats from './components/TopNStats.jsx'; import TrendIndicator from './components/TrendIndicator.jsx'; @@ -78,16 +75,10 @@ function MetricCard({ title, value, icon: Icon, color, description }) { ); } -/** Карточка сервиса с иконкой и пингом (как на референсном скрине) */ -const PING_SERVICES_CONFIG = [ - { id: 'google', name: 'Google', host: '8.8.8.8', Icon: IconBrandGoogle, color: 'blue' }, - { id: 'cloudflare', name: 'Cloudflare', host: '1.1.1.1', Icon: IconBrandCloudflare, color: 'orange' }, - { id: 'yandex', name: 'Yandex', host: 'ya.ru', Icon: IconBrandYandex, color: 'red' }, - { id: 'instagram', name: 'Instagram', host: 'instagram.com', Icon: IconBrandInstagram, color: 'pink' }, -]; - +/** Карточка сервиса с иконкой и пингом (config из API: id, name, host, icon, color) */ function PingServiceCard({ config, ms, loading }) { - const { name, host, Icon, color } = config; + const { name, host, color } = config; + const { Icon } = getIconById(config.icon); const value = loading ? '...' : (ms != null ? `${ms} мс` : '—'); return (
@@ -125,6 +116,7 @@ function Dashboard() { const [error, setError] = useState(null); const [lastFetchTime, setLastFetchTime] = useState(null); const [pingServices, setPingServices] = useState(null); + const [pingServicesConfig, setPingServicesConfig] = useState([]); const [pingLoading, setPingLoading] = useState(true); useEffect(() => { @@ -224,6 +216,18 @@ function Dashboard() { fetchStats(); }, []); + useEffect(() => { + let cancelled = false; + api.get('/ping-services-list') + .then(({ data }) => { + if (!cancelled && Array.isArray(data?.list)) setPingServicesConfig(data.list); + }) + .catch(() => { + if (!cancelled) setPingServicesConfig([]); + }); + return () => { cancelled = true; }; + }, []); + useEffect(() => { let cancelled = false; setPingLoading(true); @@ -270,9 +274,9 @@ function Dashboard() { )} /> - {/* Пинг до сервисов: Google, Cloudflare, Yandex, Instagram */} + {/* Пинг до сервисов (список из настроек / Пинг сервисов) */}
- {PING_SERVICES_CONFIG.map((config) => ( + {pingServicesConfig.map((config) => (
{ + setLoading(true); + setError(''); + try { + const res = await api.get('/ui-settings'); + const data = res?.data || {}; + setRawSettings(data); + setEtag(String(res?.headers?.etag || res?.headers?.ETag || '').replace(/"/g, '')); + const list = Array.isArray(data.pingServicesList) ? data.pingServicesList : []; + setItems(list.map((s) => ({ + id: String(s?.id ?? '').trim() || slugId(s?.name), + name: String(s?.name ?? '').trim(), + host: String(s?.host ?? '').trim(), + port: Math.max(1, parseInt(s?.port, 10) || 443), + icon: typeof s?.icon === 'string' ? s.icon : 'IconWorld', + color: typeof s?.color === 'string' ? s.color : 'primary', + })).filter((s) => s.host)); + } catch (e) { + console.error(e); + setError('Не удалось загрузить настройки'); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + loadSettings(); + }, []); + + const saveSettings = async () => { + setSaving(true); + setError(''); + setSuccess(''); + try { + const payload = { + ...rawSettings, + pingServicesList: items.map((s) => ({ + id: s.id, + name: s.name, + host: s.host, + port: s.port, + icon: s.icon, + color: s.color, + })), + }; + const res = await api.post('/ui-settings', { settings: payload, etag }); + setRawSettings(payload); + setEtag(String(res?.data?.etag || etag)); + setSuccess('Список сервисов сохранён'); + setTimeout(() => setSuccess(''), 3000); + } catch (e) { + setError(e?.response?.data?.message || 'Не удалось сохранить'); + } finally { + setSaving(false); + } + }; + + const openAdd = () => { + setNewItem({ + id: '', + name: '', + host: '', + port: '443', + icon: 'IconBrandGoogle', + color: 'blue', + }); + setAddModalOpen(true); + }; + + const submitAdd = () => { + const name = String(newItem.name || '').trim(); + const host = String(newItem.host || '').trim(); + if (!name) { setError('Укажите название'); return; } + if (!host) { setError('Укажите хост'); return; } + const id = newItem.id?.trim() || slugId(name); + if (items.some((i) => i.id === id)) { setError('Такой ID уже есть'); return; } + setError(''); + setItems((prev) => [...prev, { + id, + name, + host, + port: Math.max(1, parseInt(newItem.port, 10) || 443), + icon: newItem.icon || 'IconWorld', + color: newItem.color || 'primary', + }]); + setAddModalOpen(false); + setSuccess('Сервис добавлен. Нажмите «Сохранить».'); + setTimeout(() => setSuccess(''), 3000); + }; + + const openEdit = (item) => { + setEditingItem(item); + setEditDraft({ + id: item.id, + name: item.name, + host: item.host, + port: String(item.port), + icon: item.icon || 'IconWorld', + color: item.color || 'primary', + }); + setEditModalOpen(true); + }; + + const submitEdit = () => { + const name = String(editDraft.name || '').trim(); + const host = String(editDraft.host || '').trim(); + if (!name) { setError('Укажите название'); return; } + if (!host) { setError('Укажите хост'); return; } + const id = editDraft.id?.trim() || editingItem.id; + if (id !== editingItem.id && items.some((i) => i.id === id)) { setError('Такой ID уже есть'); return; } + setError(''); + setItems((prev) => prev.map((i) => (i.id === editingItem.id ? { + id, + name, + host, + port: Math.max(1, parseInt(editDraft.port, 10) || 443), + icon: editDraft.icon || 'IconWorld', + color: editDraft.color || 'primary', + } : i))); + setEditModalOpen(false); + setEditingItem(null); + setSuccess('Сервис обновлён. Нажмите «Сохранить».'); + setTimeout(() => setSuccess(''), 3000); + }; + + const openDelete = (item) => { + setItemToDelete(item); + setDeleteModalOpen(true); + }; + + const confirmDelete = () => { + if (itemToDelete) { + setItems((prev) => prev.filter((i) => i.id !== itemToDelete.id)); + setDeleteModalOpen(false); + setItemToDelete(null); + setSuccess('Сервис удалён. Нажмите «Сохранить».'); + setTimeout(() => setSuccess(''), 3000); + } + }; + + const [searchTerm, setSearchTerm] = useState(''); + const filtered = items.filter((i) => { + const q = searchTerm.trim().toLowerCase(); + if (!q) return true; + return (i.name || '').toLowerCase().includes(q) || (i.host || '').toLowerCase().includes(q) || (i.id || '').toLowerCase().includes(q); + }); + + return ( +
+ } + actions={( + + + + + )} + /> + + {error && setError('')} onRetry={loadSettings} />} + {success && ( +
+ {success} +
+ )} + +
+
+
+ + + + setSearchTerm(e.target.value)} + /> +
+ + {filtered.length} из {items.length} + +
+
+ + + + + + + + + + + + + + {loading ? ( + + + + ) : filtered.length === 0 ? ( + + + + ) : ( + filtered.map((item) => { + const iconInfo = getIconById(item.icon); + return ( + + + + + + + + + + ); + }) + )} + +
ИконкаIDНазваниеХостПортЦветДействия
Загрузка...
+ Нет сервисов. Нажмите «Добавить» или сохраните список — на главной будут использоваться сервисы по умолчанию. +
+ + + + {item.id}{item.name}{item.host}{item.port} + {item.color} + +
+ + +
+
+
+
+ + setAddModalOpen(false)} + onSubmit={submitAdd} + title="Добавить сервис для пинга" + submitLabel="Добавить" + > + setNewItem((p) => ({ ...p, name: v }))} placeholder="Google" /> + setNewItem((p) => ({ ...p, host: v }))} placeholder="8.8.8.8" /> + setNewItem((p) => ({ ...p, port: v }))} min={1} max={65535} /> + setNewItem((p) => ({ ...p, icon: id }))} /> +
+ + +
+ setNewItem((p) => ({ ...p, id: v }))} placeholder="auto из названия" helpText="Уникальный ключ. Пусто — из названия." /> +
+ + { setEditModalOpen(false); setEditingItem(null); }} + onSubmit={submitEdit} + title="Редактировать сервис" + > + setEditDraft((p) => ({ ...p, id: v }))} /> + setEditDraft((p) => ({ ...p, name: v }))} /> + setEditDraft((p) => ({ ...p, host: v }))} /> + setEditDraft((p) => ({ ...p, port: v }))} min={1} max={65535} /> + setEditDraft((p) => ({ ...p, icon: id }))} /> +
+ + +
+
+ + { setDeleteModalOpen(false); setItemToDelete(null); }} + /> +
+ ); +} diff --git a/frontend/src/components/IconPicker.jsx b/frontend/src/components/IconPicker.jsx new file mode 100644 index 0000000..82d73f2 --- /dev/null +++ b/frontend/src/components/IconPicker.jsx @@ -0,0 +1,105 @@ +import { useState, useMemo, useRef, useEffect } from 'react'; +import BRAND_ICONS, { getIconById } from '../lib/brandIcons.js'; +import { IconSearch } from '@tabler/icons-react'; + +/** + * Выбор иконки бренда с поиском и предпросмотром. + * value — id иконки (например IconBrandGoogle), onChange(id) — при выборе. + */ +export default function IconPicker({ value, onChange, disabled = false, label = 'Иконка бренда' }) { + const [search, setSearch] = useState(''); + const [open, setOpen] = useState(false); + + const selected = getIconById(value); + const filtered = useMemo(() => { + const q = (search || '').trim().toLowerCase(); + if (!q) return BRAND_ICONS; + return BRAND_ICONS.filter( + (item) => + item.label.toLowerCase().includes(q) || item.id.toLowerCase().includes(q) + ); + }, [search]); + + const handleSelect = (item) => { + onChange?.(item.id); + setOpen(false); + setSearch(''); + }; + + const panelRef = useRef(null); + useEffect(() => { + if (!open) return; + const handleClickOutside = (e) => { + if (panelRef.current && !panelRef.current.contains(e.target) && !e.target.closest('button[aria-expanded]')) { + setOpen(false); + } + }; + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, [open]); + + return ( +
+ +
+ + {open && ( +
+
+ + + + setSearch(e.target.value)} + autoFocus + /> +
+
+ {filtered.map((item) => ( + + ))} +
+ {filtered.length === 0 && ( +
Ничего не найдено
+ )} +
+ )} +
+
+ ); +} + +export { getIconById }; diff --git a/frontend/src/lib/brandIcons.js b/frontend/src/lib/brandIcons.js new file mode 100644 index 0000000..d9f967b --- /dev/null +++ b/frontend/src/lib/brandIcons.js @@ -0,0 +1,89 @@ +/** + * Реестр брендовых иконок (Tabler Icons) для выбора в настройках пинг-сервисов. + * id — имя компонента (например IconBrandGoogle), label — для поиска и отображения. + */ +import { + IconBrandGoogle, + IconBrandCloudflare, + IconBrandYandex, + IconBrandInstagram, + IconBrandGithub, + IconBrandTelegram, + IconBrandWhatsapp, + IconBrandFacebook, + IconBrandX, + IconBrandYoutube, + IconBrandTiktok, + IconBrandAmazon, + IconBrandApple, + IconBrandMicrosoft, + IconBrandDiscord, + IconBrandSpotify, + IconBrandVk, + IconBrandReddit, + IconBrandLinkedin, + IconBrandPinterest, + IconBrandTwitch, + IconBrandSteam, + IconBrandNginx, + IconBrandDocker, + IconBrandChrome, + IconBrandFirefox, + IconBrandSafari, + IconBrandEdge, + IconBrandSlack, + IconBrandZoom, + IconBrandSkype, + IconBrandPaypal, + IconBrandStripe, + IconBrandVisa, + IconBrandMastercard, + IconWorld, +} from '@tabler/icons-react'; + +const BRAND_ICONS = [ + { id: 'IconBrandGoogle', label: 'Google', Icon: IconBrandGoogle }, + { id: 'IconBrandCloudflare', label: 'Cloudflare', Icon: IconBrandCloudflare }, + { id: 'IconBrandYandex', label: 'Yandex', Icon: IconBrandYandex }, + { id: 'IconBrandInstagram', label: 'Instagram', Icon: IconBrandInstagram }, + { id: 'IconBrandGithub', label: 'GitHub', Icon: IconBrandGithub }, + { id: 'IconBrandTelegram', label: 'Telegram', Icon: IconBrandTelegram }, + { id: 'IconBrandWhatsapp', label: 'WhatsApp', Icon: IconBrandWhatsapp }, + { id: 'IconBrandFacebook', label: 'Facebook', Icon: IconBrandFacebook }, + { id: 'IconBrandX', label: 'X (Twitter)', Icon: IconBrandX }, + { id: 'IconBrandYoutube', label: 'YouTube', Icon: IconBrandYoutube }, + { id: 'IconBrandTiktok', label: 'TikTok', Icon: IconBrandTiktok }, + { id: 'IconBrandAmazon', label: 'Amazon', Icon: IconBrandAmazon }, + { id: 'IconBrandApple', label: 'Apple', Icon: IconBrandApple }, + { id: 'IconBrandMicrosoft', label: 'Microsoft', Icon: IconBrandMicrosoft }, + { id: 'IconBrandDiscord', label: 'Discord', Icon: IconBrandDiscord }, + { id: 'IconBrandSpotify', label: 'Spotify', Icon: IconBrandSpotify }, + { id: 'IconBrandVk', label: 'VK', Icon: IconBrandVk }, + { id: 'IconBrandReddit', label: 'Reddit', Icon: IconBrandReddit }, + { id: 'IconBrandLinkedin', label: 'LinkedIn', Icon: IconBrandLinkedin }, + { id: 'IconBrandPinterest', label: 'Pinterest', Icon: IconBrandPinterest }, + { id: 'IconBrandTwitch', label: 'Twitch', Icon: IconBrandTwitch }, + { id: 'IconBrandSteam', label: 'Steam', Icon: IconBrandSteam }, + { id: 'IconBrandNginx', label: 'Nginx', Icon: IconBrandNginx }, + { id: 'IconBrandDocker', label: 'Docker', Icon: IconBrandDocker }, + { id: 'IconBrandChrome', label: 'Chrome', Icon: IconBrandChrome }, + { id: 'IconBrandFirefox', label: 'Firefox', Icon: IconBrandFirefox }, + { id: 'IconBrandSafari', label: 'Safari', Icon: IconBrandSafari }, + { id: 'IconBrandEdge', label: 'Edge', Icon: IconBrandEdge }, + { id: 'IconBrandSlack', label: 'Slack', Icon: IconBrandSlack }, + { id: 'IconBrandZoom', label: 'Zoom', Icon: IconBrandZoom }, + { id: 'IconBrandSkype', label: 'Skype', Icon: IconBrandSkype }, + { id: 'IconBrandPaypal', label: 'PayPal', Icon: IconBrandPaypal }, + { id: 'IconBrandStripe', label: 'Stripe', Icon: IconBrandStripe }, + { id: 'IconBrandVisa', label: 'Visa', Icon: IconBrandVisa }, + { id: 'IconBrandMastercard', label: 'Mastercard', Icon: IconBrandMastercard }, + { id: 'IconWorld', label: 'Сеть (World)', Icon: IconWorld }, +]; + +const ICON_BY_ID = Object.fromEntries(BRAND_ICONS.map((item) => [item.id, item])); + +export function getIconById(id) { + return ICON_BY_ID[id] || BRAND_ICONS[0]; +} + +export default BRAND_ICONS;