From 82e15e708ffe258d7030883b4b3eeb4d3efac7a0 Mon Sep 17 00:00:00 2001 From: Denis Shatskiy Date: Fri, 8 Aug 2025 09:54:34 +0700 Subject: [PATCH] feat: Enhance DomainsNewManager with validation for domain and community fields, add import/export functionality, and improve UI with new icons and feedback messages --- frontend/src/DomainsNewManager.jsx | 106 ++++++++++++++++++++++++----- 1 file changed, 90 insertions(+), 16 deletions(-) diff --git a/frontend/src/DomainsNewManager.jsx b/frontend/src/DomainsNewManager.jsx index 56b7214..b76a286 100644 --- a/frontend/src/DomainsNewManager.jsx +++ b/frontend/src/DomainsNewManager.jsx @@ -8,7 +8,10 @@ import { IconCheck, IconX, IconDatabase, - IconRefresh + IconRefresh, + IconUpload, + IconDownload, + IconDeviceFloppy } from '@tabler/icons-react'; const API_URL = '/api'; @@ -16,6 +19,7 @@ const API_URL = '/api'; function DomainsNewManager() { const [items, setItems] = useState([]); const [newItem, setNewItem] = useState({ domain: '', community: '' }); + const [newInvalid, setNewInvalid] = useState({ domain: false, community: false }); const [error, setError] = useState(''); const [success, setSuccess] = useState(''); const [searchTerm, setSearchTerm] = useState(''); @@ -31,6 +35,14 @@ function DomainsNewManager() { const editInputRef = useRef(null); const pageSize = 10; + const isValidDomain = (value) => { + const v = String(value).trim().toLowerCase(); + // Простой валидатор FQDN: sub.domain.tld + return /^([a-z0-9-]+\.)+[a-z]{2,}$/i.test(v); + }; + + const isValidCommunity = (value) => /^[0-9]+$/.test(String(value).trim()); + useEffect(() => { fetchItems(); }, []); @@ -50,17 +62,17 @@ function DomainsNewManager() { }; const handleAddItem = () => { - if (newItem.domain.trim() === '') { - setError('Имя домена не может быть пустым.'); - return; - } - if (newItem.community.trim() === '') { - setError('Community не может быть пустым.'); + const domainOk = isValidDomain(newItem.domain); + const communityOk = isValidCommunity(newItem.community); + setNewInvalid({ domain: !domainOk, community: !communityOk }); + if (!domainOk || !communityOk) { + setError('Заполните корректный домен (example.com) и числовой community.'); return; } setError(''); - setItems([...items, newItem]); + setItems([...items, { domain: newItem.domain.trim().toLowerCase(), community: String(newItem.community).trim() }]); setNewItem({ domain: '', community: '' }); + setNewInvalid({ domain: false, community: false }); }; const handleEdit = (item) => { @@ -69,8 +81,12 @@ function DomainsNewManager() { }; const handleSaveEdit = (domainName) => { + if (!isValidCommunity(editingValue)) { + setError('Community должен быть числом.'); + return; + } const updatedItems = items.map(i => - i.domain === domainName ? { ...i, community: editingValue } : i + i.domain === domainName ? { ...i, community: String(editingValue).trim() } : i ); setItems(updatedItems); setEditingDomain(null); @@ -100,7 +116,9 @@ function DomainsNewManager() { const handleSaveChanges = async () => { setLoading(true); try { - await axios.post(`${API_URL}/domains-new`, { domains: items }); + // сохраняем только валидные строки + const valid = items.filter(i => isValidDomain(i.domain) && isValidCommunity(i.community)); + await axios.post(`${API_URL}/domains-new`, { domains: valid }); setSuccess('Изменения успешно сохранены!'); setTimeout(() => setSuccess(''), 3000); } catch (error) { @@ -111,6 +129,37 @@ function DomainsNewManager() { } }; + const handleImport = () => { + const text = window.prompt('Вставьте строки: DOMAIN ПРОБЕЛ COMMUNITY (по одной записи на строку)'); + if (!text) return; + const lines = text.split(/\r?\n/).map(l => l.trim()).filter(Boolean); + const parsed = lines.map(l => { + const [d, c] = l.split(/\s+/); + return { domain: (d || '').toLowerCase(), community: c || '' }; + }); + setItems(prev => [...prev, ...parsed]); + }; + + const handleExport = () => { + const header = ['domain', 'community']; + const csv = [header, ...items.map(i => [i.domain, i.community])] + .map(r => r.map(x => `"${(x ?? '').toString().replace(/"/g, '""')}"`).join(',')) + .join('\n'); + const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `domains_${new Date().toISOString().split('T')[0]}.csv`; + a.click(); + URL.revokeObjectURL(url); + }; + + const clearInvalid = () => { + setItems(prev => prev.filter(i => i.domain || i.community) + .filter(i => isValidDomain(i.domain) && isValidCommunity(i.community)) + ); + }; + // Сортировка const sortedItems = [...items].sort((a, b) => { let valA = a[sortField] || ''; @@ -207,21 +256,24 @@ function DomainsNewManager() { setNewItem({ ...newItem, domain: e.target.value })} /> + {newInvalid.domain &&
Неверный формат домена
} +
Без http/https, только доменное имя
setNewItem({ ...newItem, community: e.target.value })} /> + {newInvalid.community &&
Только цифры
}
+ + +
@@ -276,7 +350,7 @@ function DomainsNewManager() {
-

Список доменов

+

Список доменов {items.length}

{/* Фильтр по community */} setEditingValue(e.target.value)} onKeyDown={e => handleEditKeyDown(e, item.domain)} style={{maxWidth: 120}} /> - + ) : (