feat(communities): add icon support to community model and UI components for enhanced visual representation
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m58s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m58s
This commit is contained in:
@@ -24,7 +24,8 @@ async function getCommunities(req, res) {
|
|||||||
description: c.description ? String(c.description) : '',
|
description: c.description ? String(c.description) : '',
|
||||||
tags: Array.isArray(c.tags) ? c.tags.map(String) : [],
|
tags: Array.isArray(c.tags) ? c.tags.map(String) : [],
|
||||||
gatewayDefault: c.gatewayDefault ? String(c.gatewayDefault) : '',
|
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: []
|
defaultValue: []
|
||||||
@@ -70,6 +71,7 @@ async function postCommunities(req, res) {
|
|||||||
tags: Array.isArray(entry.tags) ? entry.tags.map(String) : [],
|
tags: Array.isArray(entry.tags) ? entry.tags.map(String) : [],
|
||||||
gatewayDefault: entry.gatewayDefault ? String(entry.gatewayDefault) : '',
|
gatewayDefault: entry.gatewayDefault ? String(entry.gatewayDefault) : '',
|
||||||
color: entry.color ? String(entry.color) : '',
|
color: entry.color ? String(entry.color) : '',
|
||||||
|
icon: entry.icon ? String(entry.icon) : '',
|
||||||
category: entry.category ? String(entry.category) : '',
|
category: entry.category ? String(entry.category) : '',
|
||||||
priority: typeof entry.priority === 'number' ? entry.priority : 0,
|
priority: typeof entry.priority === 'number' ? entry.priority : 0,
|
||||||
enabled: typeof entry.enabled === 'boolean' ? entry.enabled : true,
|
enabled: typeof entry.enabled === 'boolean' ? entry.enabled : true,
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import {
|
|||||||
IconFilter,
|
IconFilter,
|
||||||
IconChartBar,
|
IconChartBar,
|
||||||
IconList,
|
IconList,
|
||||||
IconLink,
|
|
||||||
IconRefresh,
|
IconRefresh,
|
||||||
IconSortAscending,
|
IconSortAscending,
|
||||||
IconSortDescending,
|
IconSortDescending,
|
||||||
@@ -29,6 +28,8 @@ import ConfirmModal from './components/ConfirmModal.jsx';
|
|||||||
import FormField from './components/FormField.jsx';
|
import FormField from './components/FormField.jsx';
|
||||||
import ErrorAlert from './components/ErrorAlert.jsx';
|
import ErrorAlert from './components/ErrorAlert.jsx';
|
||||||
import Pagination from './components/Pagination.jsx';
|
import Pagination from './components/Pagination.jsx';
|
||||||
|
import IconPicker from './components/IconPicker.jsx';
|
||||||
|
import { getIconById } from './lib/brandIcons.js';
|
||||||
|
|
||||||
const API_URL = '/api';
|
const API_URL = '/api';
|
||||||
|
|
||||||
@@ -53,9 +54,9 @@ function CommunitiesManager() {
|
|||||||
// Состояние для переключения между списком и статистикой
|
// Состояние для переключения между списком и статистикой
|
||||||
const [activeTab, setActiveTab] = useState('list'); // 'list' | 'stats'
|
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 [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 [filterUsage, setFilterUsage] = useState({});
|
||||||
const [filterUsageLoading, setFilterUsageLoading] = useState(false);
|
const [filterUsageLoading, setFilterUsageLoading] = useState(false);
|
||||||
const [filterUsageError, setFilterUsageError] = useState('');
|
const [filterUsageError, setFilterUsageError] = useState('');
|
||||||
@@ -79,7 +80,7 @@ function CommunitiesManager() {
|
|||||||
Object.keys(filterUsage).forEach((value) => {
|
Object.keys(filterUsage).forEach((value) => {
|
||||||
if (!value) return;
|
if (!value) return;
|
||||||
if (!map.has(value)) {
|
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());
|
return Array.from(map.values());
|
||||||
@@ -130,13 +131,14 @@ function CommunitiesManager() {
|
|||||||
tags: Array.isArray(d.tags) ? d.tags : [],
|
tags: Array.isArray(d.tags) ? d.tags : [],
|
||||||
gatewayDefault: String(d.gatewayDefault || ''),
|
gatewayDefault: String(d.gatewayDefault || ''),
|
||||||
color: String(d.color || ''),
|
color: String(d.color || ''),
|
||||||
|
icon: String(d.icon || ''),
|
||||||
_external: false,
|
_external: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// add unknown ones from usage
|
// add unknown ones from usage
|
||||||
for (const v of usedCommunities) {
|
for (const v of usedCommunities) {
|
||||||
if (!map.has(v)) {
|
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()));
|
setItems(Array.from(map.values()));
|
||||||
@@ -217,8 +219,9 @@ function CommunitiesManager() {
|
|||||||
tags: toTagsArray(newItem.tags),
|
tags: toTagsArray(newItem.tags),
|
||||||
gatewayDefault: String(newItem.gatewayDefault || ''),
|
gatewayDefault: String(newItem.gatewayDefault || ''),
|
||||||
color: String(newItem.color || ''),
|
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);
|
setAddModalOpen(false);
|
||||||
setSuccess('Community успешно добавлен!');
|
setSuccess('Community успешно добавлен!');
|
||||||
setTimeout(() => setSuccess(''), 3000);
|
setTimeout(() => setSuccess(''), 3000);
|
||||||
@@ -233,6 +236,7 @@ function CommunitiesManager() {
|
|||||||
tags: fromTagsArray(item.tags),
|
tags: fromTagsArray(item.tags),
|
||||||
gatewayDefault: item.gatewayDefault || '',
|
gatewayDefault: item.gatewayDefault || '',
|
||||||
color: item.color || '',
|
color: item.color || '',
|
||||||
|
icon: item.icon || '',
|
||||||
});
|
});
|
||||||
setEditModalOpen(true);
|
setEditModalOpen(true);
|
||||||
};
|
};
|
||||||
@@ -249,6 +253,7 @@ function CommunitiesManager() {
|
|||||||
tags: toTagsArray(editingDraft.tags),
|
tags: toTagsArray(editingDraft.tags),
|
||||||
gatewayDefault: String(editingDraft.gatewayDefault || ''),
|
gatewayDefault: String(editingDraft.gatewayDefault || ''),
|
||||||
color: String(editingDraft.color || ''),
|
color: String(editingDraft.color || ''),
|
||||||
|
icon: String(editingDraft.icon || ''),
|
||||||
} : i);
|
} : i);
|
||||||
setItems(next);
|
setItems(next);
|
||||||
setEditingValue(null);
|
setEditingValue(null);
|
||||||
@@ -278,7 +283,7 @@ function CommunitiesManager() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleImport = () => {
|
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;
|
if (!text) return;
|
||||||
try {
|
try {
|
||||||
const arr = JSON.parse(text);
|
const arr = JSON.parse(text);
|
||||||
@@ -297,6 +302,7 @@ function CommunitiesManager() {
|
|||||||
tags: Array.isArray(x.tags) ? x.tags : [],
|
tags: Array.isArray(x.tags) ? x.tags : [],
|
||||||
gatewayDefault: String(x.gatewayDefault || ''),
|
gatewayDefault: String(x.gatewayDefault || ''),
|
||||||
color: String(x.color || ''),
|
color: String(x.color || ''),
|
||||||
|
icon: String(x.icon || ''),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
return Array.from(map.values());
|
return Array.from(map.values());
|
||||||
@@ -553,6 +559,7 @@ function CommunitiesManager() {
|
|||||||
<table className="table card-table table-vcenter table-nowrap mb-0">
|
<table className="table card-table table-vcenter table-nowrap mb-0">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
<th style={{ width: '8%' }}>Иконка</th>
|
||||||
<th className="cursor-pointer user-select-none" onClick={()=>changeSort('value')}>
|
<th className="cursor-pointer user-select-none" onClick={()=>changeSort('value')}>
|
||||||
<div className="d-flex align-items-center">
|
<div className="d-flex align-items-center">
|
||||||
<IconHash size={16} className="me-1 text-muted" />
|
<IconHash size={16} className="me-1 text-muted" />
|
||||||
@@ -584,9 +591,9 @@ function CommunitiesManager() {
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<tr><td colSpan={7} className="text-center text-muted py-4">Загрузка...</td></tr>
|
<tr><td colSpan={8} className="text-center text-muted py-4">Загрузка...</td></tr>
|
||||||
) : paginated.length === 0 ? (
|
) : paginated.length === 0 ? (
|
||||||
<tr><td colSpan={7} className="text-center text-muted py-4">
|
<tr><td colSpan={8} className="text-center text-muted py-4">
|
||||||
<EmptyState
|
<EmptyState
|
||||||
title="Нет community"
|
title="Нет community"
|
||||||
description="Добавьте запись, чтобы начать."
|
description="Добавьте запись, чтобы начать."
|
||||||
@@ -600,8 +607,18 @@ function CommunitiesManager() {
|
|||||||
</td></tr>
|
</td></tr>
|
||||||
) : paginated.map((item) => {
|
) : paginated.map((item) => {
|
||||||
const usageCount = filterUsage[item.value] || 0;
|
const usageCount = filterUsage[item.value] || 0;
|
||||||
|
const iconInfo = item.icon ? getIconById(item.icon) : null;
|
||||||
return (
|
return (
|
||||||
<tr key={item.value}>
|
<tr key={item.value}>
|
||||||
|
<td>
|
||||||
|
{iconInfo ? (
|
||||||
|
<span className="avatar avatar-xs bg-blue-lt text-blue border">
|
||||||
|
<iconInfo.Icon size={14} stroke={1.5} />
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="text-muted">—</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<code className="text-blue">{item.value}</code>
|
<code className="text-blue">{item.value}</code>
|
||||||
{item._external && (
|
{item._external && (
|
||||||
@@ -629,13 +646,6 @@ function CommunitiesManager() {
|
|||||||
</td>
|
</td>
|
||||||
<td className="text-end">
|
<td className="text-end">
|
||||||
<div className="btn-list">
|
<div className="btn-list">
|
||||||
<a
|
|
||||||
className="btn btn-outline-primary btn-icon btn-sm"
|
|
||||||
href={`/filters?community=${encodeURIComponent(item.value)}`}
|
|
||||||
title="Открыть в /filters с поиском по community"
|
|
||||||
>
|
|
||||||
<IconLink size={16} />
|
|
||||||
</a>
|
|
||||||
<button
|
<button
|
||||||
className="btn btn-outline-primary btn-icon btn-sm"
|
className="btn btn-outline-primary btn-icon btn-sm"
|
||||||
onClick={()=>startEdit(item)}
|
onClick={()=>startEdit(item)}
|
||||||
@@ -681,7 +691,7 @@ function CommunitiesManager() {
|
|||||||
show={addModalOpen}
|
show={addModalOpen}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setAddModalOpen(false);
|
setAddModalOpen(false);
|
||||||
setNewItem({ value: '', name: '', description: '', tags: '', gatewayDefault: '', color: '' });
|
setNewItem({ value: '', name: '', description: '', tags: '', gatewayDefault: '', color: '', icon: '' });
|
||||||
setError('');
|
setError('');
|
||||||
}}
|
}}
|
||||||
onSubmit={addItem}
|
onSubmit={addItem}
|
||||||
@@ -758,6 +768,12 @@ function CommunitiesManager() {
|
|||||||
placeholder="blue, green, red ..."
|
placeholder="blue, green, red ..."
|
||||||
helpText="Цвет для отображения бейджа в интерфейсе"
|
helpText="Цвет для отображения бейджа в интерфейсе"
|
||||||
/>
|
/>
|
||||||
|
<IconPicker
|
||||||
|
value={newItem.icon}
|
||||||
|
onChange={(id) => setNewItem({...newItem, icon: id})}
|
||||||
|
label="Иконка бренда (опционально)"
|
||||||
|
allowNone
|
||||||
|
/>
|
||||||
</FormModal>
|
</FormModal>
|
||||||
|
|
||||||
{/* Модальное окно редактирования */}
|
{/* Модальное окно редактирования */}
|
||||||
@@ -840,6 +856,12 @@ function CommunitiesManager() {
|
|||||||
placeholder="blue, green, red ..."
|
placeholder="blue, green, red ..."
|
||||||
helpText="Цвет для отображения бейджа в интерфейсе"
|
helpText="Цвет для отображения бейджа в интерфейсе"
|
||||||
/>
|
/>
|
||||||
|
<IconPicker
|
||||||
|
value={editingDraft.icon}
|
||||||
|
onChange={(id) => setEditingDraft({...editingDraft, icon: id})}
|
||||||
|
label="Иконка бренда (опционально)"
|
||||||
|
allowNone
|
||||||
|
/>
|
||||||
</FormModal>
|
</FormModal>
|
||||||
|
|
||||||
{/* Модальное окно подтверждения удаления */}
|
{/* Модальное окно подтверждения удаления */}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { IconHash } from '@tabler/icons-react';
|
import { IconHash } from '@tabler/icons-react';
|
||||||
|
import { getIconById } from '../lib/brandIcons.js';
|
||||||
|
|
||||||
function CommunityAutocompleteInput({
|
function CommunityAutocompleteInput({
|
||||||
value,
|
value,
|
||||||
@@ -105,6 +106,7 @@ function CommunityAutocompleteInput({
|
|||||||
const label = s.name ? `${s.value} - ${s.name}` : `${s.value}`;
|
const label = s.name ? `${s.value} - ${s.name}` : `${s.value}`;
|
||||||
const description = s.description || '';
|
const description = s.description || '';
|
||||||
const tags = Array.isArray(s.tags) ? s.tags : [];
|
const tags = Array.isArray(s.tags) ? s.tags : [];
|
||||||
|
const iconInfo = s.icon ? getIconById(s.icon) : null;
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -114,9 +116,15 @@ function CommunityAutocompleteInput({
|
|||||||
onMouseEnter={() => setActiveIndex(idx)}
|
onMouseEnter={() => setActiveIndex(idx)}
|
||||||
>
|
>
|
||||||
<div className="d-flex align-items-start">
|
<div className="d-flex align-items-start">
|
||||||
<span className="avatar me-2 bg-blue-lt text-blue border-0" style={{ width: 22, height: 22 }}>
|
{iconInfo ? (
|
||||||
<IconHash size={14} />
|
<span className="avatar me-2 bg-blue-lt text-blue border-0" style={{ width: 22, height: 22 }}>
|
||||||
</span>
|
<iconInfo.Icon size={14} stroke={1.5} />
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="avatar me-2 bg-blue-lt text-blue border-0" style={{ width: 22, height: 22 }}>
|
||||||
|
<IconHash size={14} />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
<div className="flex-fill text-start">
|
<div className="flex-fill text-start">
|
||||||
<div className="fw-medium">{label}</div>
|
<div className="fw-medium">{label}</div>
|
||||||
{(description || tags.length > 0) && (
|
{(description || tags.length > 0) && (
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { getIconById } from '../lib/brandIcons.js';
|
||||||
|
|
||||||
function normalizeTablerColor(input) {
|
function normalizeTablerColor(input) {
|
||||||
const allowed = new Set(['blue','azure','indigo','purple','pink','red','orange','yellow','lime','green','teal','cyan','grape','gray']);
|
const allowed = new Set(['blue','azure','indigo','purple','pink','red','orange','yellow','lime','green','teal','cyan','grape','gray']);
|
||||||
const c = String(input || 'blue').toLowerCase().replace(/[^a-z-]/g, '');
|
const c = String(input || 'blue').toLowerCase().replace(/[^a-z-]/g, '');
|
||||||
@@ -11,8 +13,12 @@ function CommunityBadge({ value, communities = [] }) {
|
|||||||
const color = normalizeTablerColor(meta?.color || 'blue');
|
const color = normalizeTablerColor(meta?.color || 'blue');
|
||||||
const cls = `badge bg-${color}-lt text-${color}`;
|
const cls = `badge bg-${color}-lt text-${color}`;
|
||||||
const title = meta ? `${meta.name ? meta.name + ' — ' : ''}${meta.description || ''}${Array.isArray(meta.tags) && meta.tags.length ? ' (' + meta.tags.join(', ') + ')' : ''}` : '';
|
const title = meta ? `${meta.name ? meta.name + ' — ' : ''}${meta.description || ''}${Array.isArray(meta.tags) && meta.tags.length ? ' (' + meta.tags.join(', ') + ')' : ''}` : '';
|
||||||
|
const iconInfo = meta?.icon ? getIconById(meta.icon) : null;
|
||||||
return (
|
return (
|
||||||
<span className={cls} title={title}>{v}</span>
|
<span className={cls} title={title}>
|
||||||
|
{iconInfo ? <iconInfo.Icon size={12} stroke={1.8} className="me-1" /> : null}
|
||||||
|
{v}
|
||||||
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
import { useState, useMemo, useRef, useEffect } from 'react';
|
import { useState, useMemo, useRef, useEffect } from 'react';
|
||||||
import BRAND_ICONS, { getIconById } from '../lib/brandIcons.js';
|
import BRAND_ICONS, { getIconById } from '../lib/brandIcons.js';
|
||||||
import { IconSearch } from '@tabler/icons-react';
|
import { IconSearch, IconMinus } from '@tabler/icons-react';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Выбор иконки бренда с поиском и предпросмотром.
|
* Выбор иконки бренда с поиском и предпросмотром.
|
||||||
* value — id иконки (например IconBrandGoogle), onChange(id) — при выборе.
|
* value — id иконки (например IconBrandGoogle), onChange(id) — при выборе.
|
||||||
*/
|
*/
|
||||||
export default function IconPicker({ value, onChange, disabled = false, label = 'Иконка бренда' }) {
|
export default function IconPicker({ value, onChange, disabled = false, label = 'Иконка бренда', allowNone = false }) {
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
const selected = getIconById(value);
|
const selected = allowNone && !value
|
||||||
const filtered = useMemo(() => {
|
? { id: '', label: 'Без иконки', Icon: IconMinus }
|
||||||
|
: getIconById(value);
|
||||||
|
const filteredIcons = useMemo(() => {
|
||||||
const q = (search || '').trim().toLowerCase();
|
const q = (search || '').trim().toLowerCase();
|
||||||
if (!q) return BRAND_ICONS;
|
if (!q) return BRAND_ICONS;
|
||||||
return BRAND_ICONS.filter(
|
return BRAND_ICONS.filter(
|
||||||
@@ -19,6 +21,13 @@ export default function IconPicker({ value, onChange, disabled = false, label =
|
|||||||
item.label.toLowerCase().includes(q) || item.id.toLowerCase().includes(q)
|
item.label.toLowerCase().includes(q) || item.id.toLowerCase().includes(q)
|
||||||
);
|
);
|
||||||
}, [search]);
|
}, [search]);
|
||||||
|
const filtered = useMemo(() => {
|
||||||
|
if (!allowNone) return filteredIcons;
|
||||||
|
const q = (search || '').trim().toLowerCase();
|
||||||
|
const noneMatches = q.length === 0 || 'без иконки'.includes(q) || 'none'.includes(q);
|
||||||
|
if (!noneMatches) return filteredIcons;
|
||||||
|
return [{ id: '', label: 'Без иконки', Icon: IconMinus }, ...filteredIcons];
|
||||||
|
}, [allowNone, filteredIcons, search]);
|
||||||
|
|
||||||
const handleSelect = (item) => {
|
const handleSelect = (item) => {
|
||||||
onChange?.(item.id);
|
onChange?.(item.id);
|
||||||
@@ -56,7 +65,7 @@ export default function IconPicker({ value, onChange, disabled = false, label =
|
|||||||
<span>{selected.label}</span>
|
<span>{selected.label}</span>
|
||||||
</button>
|
</button>
|
||||||
{open && (
|
{open && (
|
||||||
<div className="border rounded p-2 shadow-sm bg-body" style={{ minWidth: 280, maxWidth: 360 }}>
|
<div className="border rounded p-3 shadow-sm bg-body" style={{ minWidth: 360, maxWidth: 520 }}>
|
||||||
<div className="input-group input-group-sm mb-2">
|
<div className="input-group input-group-sm mb-2">
|
||||||
<span className="input-group-text">
|
<span className="input-group-text">
|
||||||
<IconSearch size={14} />
|
<IconSearch size={14} />
|
||||||
@@ -72,7 +81,7 @@ export default function IconPicker({ value, onChange, disabled = false, label =
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="d-flex flex-wrap gap-1 overflow-auto"
|
className="d-flex flex-wrap gap-1 overflow-auto"
|
||||||
style={{ maxHeight: 200 }}
|
style={{ maxHeight: 260 }}
|
||||||
role="listbox"
|
role="listbox"
|
||||||
>
|
>
|
||||||
{filtered.map((item) => (
|
{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 ${
|
className={`btn btn-sm d-flex align-items-center justify-content-center rounded ${
|
||||||
value === item.id ? 'btn-primary' : 'btn-outline-secondary'
|
value === item.id ? 'btn-primary' : 'btn-outline-secondary'
|
||||||
}`}
|
}`}
|
||||||
style={{ width: 40, height: 40 }}
|
style={{ width: 48, height: 48 }}
|
||||||
onClick={() => handleSelect(item)}
|
onClick={() => handleSelect(item)}
|
||||||
title={item.label}
|
title={item.label}
|
||||||
role="option"
|
role="option"
|
||||||
|
|||||||
@@ -37,11 +37,14 @@ import {
|
|||||||
IconBrandVisa,
|
IconBrandVisa,
|
||||||
IconBrandMastercard,
|
IconBrandMastercard,
|
||||||
IconWorld,
|
IconWorld,
|
||||||
|
IconCloud,
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
|
|
||||||
const BRAND_ICONS = [
|
const BRAND_ICONS = [
|
||||||
{ id: 'IconBrandGoogle', label: 'Google', Icon: IconBrandGoogle },
|
{ id: 'IconBrandGoogle', label: 'Google', Icon: IconBrandGoogle },
|
||||||
{ id: 'IconBrandCloudflare', label: 'Cloudflare', Icon: IconBrandCloudflare },
|
{ 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: 'IconBrandYandex', label: 'Yandex', Icon: IconBrandYandex },
|
||||||
{ id: 'IconBrandInstagram', label: 'Instagram', Icon: IconBrandInstagram },
|
{ id: 'IconBrandInstagram', label: 'Instagram', Icon: IconBrandInstagram },
|
||||||
{ id: 'IconBrandGithub', label: 'GitHub', Icon: IconBrandGithub },
|
{ id: 'IconBrandGithub', label: 'GitHub', Icon: IconBrandGithub },
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ export interface Community {
|
|||||||
name?: string;
|
name?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
gatewayDefault?: string;
|
gatewayDefault?: string;
|
||||||
|
icon?: string;
|
||||||
category?: string;
|
category?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user