feat: Enhance AutoUrlManager with validation for URL and community fields, add mass import functionality, and improve UI with new icons and feedback messages
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 9m16s

This commit is contained in:
2025-08-08 07:58:08 +07:00
parent 9b253e8963
commit d96f1e1c24
+118 -38
View File
@@ -6,7 +6,10 @@ import {
IconDownload, IconDownload,
IconAlertCircle, IconAlertCircle,
IconCheck, IconCheck,
IconLoader IconLoader,
IconUpload,
IconDeviceFloppy,
IconInfoCircle
} from '@tabler/icons-react'; } from '@tabler/icons-react';
function AutoUrlManager() { function AutoUrlManager() {
@@ -16,6 +19,7 @@ function AutoUrlManager() {
const [processing, setProcessing] = useState(false); const [processing, setProcessing] = useState(false);
const [message, setMessage] = useState(''); const [message, setMessage] = useState('');
const [messageType, setMessageType] = useState(''); const [messageType, setMessageType] = useState('');
const [touched, setTouched] = useState({});
useEffect(() => { useEffect(() => {
fetchUrls(); fetchUrls();
@@ -47,17 +51,40 @@ function AutoUrlManager() {
const newUrls = [...urls]; const newUrls = [...urls];
newUrls[index][field] = value; newUrls[index][field] = value;
setUrls(newUrls); setUrls(newUrls);
setTouched(prev => ({ ...prev, [index]: true }));
}; };
const isValidHttpUrl = (value) => {
try {
const u = new URL(value);
return u.protocol === 'http:' || u.protocol === 'https:';
} catch {
return false;
}
};
const isValidCommunity = (value) => {
return /^[0-9]+$/.test(String(value).trim());
};
const getRowValidity = (row) => ({
url: row.url ? isValidHttpUrl(row.url) : false,
community: row.community ? isValidCommunity(row.community) : false
});
const hasAtLeastOneValidRow = urls.some(u => isValidHttpUrl(u.url) && isValidCommunity(u.community));
const saveUrls = async () => { const saveUrls = async () => {
try { try {
setSaving(true); setSaving(true);
setMessage(''); setMessage('');
// Validate URLs // Validate URLs
const validUrls = urls.filter(u => u.url.trim() && u.community.trim()); const validUrls = urls
.filter(u => isValidHttpUrl(u.url) && isValidCommunity(u.community))
.map(u => ({ url: u.url.trim(), community: String(u.community).trim() }));
if (validUrls.length === 0) { if (validUrls.length === 0) {
setMessage('Добавьте хотя бы один URL с community'); setMessage('Добавьте хотя бы одну корректную запись (валидный URL и числовой community)');
setMessageType('error'); setMessageType('error');
return; return;
} }
@@ -95,7 +122,7 @@ function AutoUrlManager() {
if (loading) { if (loading) {
return ( return (
<div className="d-flex justify-content-center align-items-center" style={{ minHeight: '200px' }}> <div className="d-flex justify-content-center align-items-center" style={{ minHeight: '200px' }}>
<IconLoader className="animate-spin" size={32} /> <div className="spinner-border" role="status" aria-label="Загрузка"></div>
</div> </div>
); );
} }
@@ -125,14 +152,34 @@ function AutoUrlManager() {
<div className="card-header"> <div className="card-header">
<h3 className="card-title">URL-адреса для автоматической загрузки</h3> <h3 className="card-title">URL-адреса для автоматической загрузки</h3>
<div className="card-actions"> <div className="card-actions">
<button <div className="btn-list">
className="btn btn-primary btn-sm" <button
onClick={addUrl} className="btn btn-outline-secondary btn-sm"
disabled={saving || processing} onClick={() => {
> const text = window.prompt('Вставьте строки вида: URL ПРОБЕЛ COMMUNITY (по одной записи на строку)');
<IconPlus className="me-1" /> if (!text) return;
Добавить URL const lines = text.split(/\r?\n/).map(l => l.trim()).filter(Boolean);
</button> const parsed = lines.map(l => {
const [u, c] = l.split(/\s+/);
return { url: u || '', community: c || '' };
});
setUrls(prev => [...prev, ...parsed]);
}}
disabled={saving || processing}
title="Массовое добавление из буфера"
>
<IconUpload className="me-1" />
Импорт
</button>
<button
className="btn btn-primary btn-sm"
onClick={addUrl}
disabled={saving || processing}
>
<IconPlus className="me-1" />
Добавить URL
</button>
</div>
</div> </div>
</div> </div>
<div className="card-body"> <div className="card-body">
@@ -151,38 +198,60 @@ function AutoUrlManager() {
</div> </div>
) : ( ) : (
<div className="table-responsive"> <div className="table-responsive">
<table className="table table-vcenter"> <table className="table card-table table-vcenter table-nowrap mb-0">
<thead> <thead>
<tr> <tr>
<th>URL</th> <th>URL</th>
<th>Community</th> <th>Community</th>
<th width="100">Действия</th> <th className="text-end" style={{ width: 100 }}>Действия</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{urls.map((url, index) => ( {urls.map((url, index) => (
<tr key={index}> <tr key={index}>
<td> <td>
<input {(() => {
type="text" const v = getRowValidity(url);
className="form-control" const invalid = touched[index] && !v.url;
placeholder="https://example.com/ips.txt" return (
value={url.url} <>
onChange={(e) => updateUrl(index, 'url', e.target.value)} <input
disabled={saving || processing} type="text"
/> className={`form-control${invalid ? ' is-invalid' : ''}`}
placeholder="https://example.com/ips.txt"
value={url.url}
onChange={(e) => updateUrl(index, 'url', e.target.value)}
disabled={saving || processing}
/>
{invalid && (
<div className="invalid-feedback">Укажите корректный http/https URL</div>
)}
</>
);
})()}
</td> </td>
<td> <td>
<input {(() => {
type="text" const v = getRowValidity(url);
className="form-control" const invalid = touched[index] && !v.community;
placeholder="555" return (
value={url.community} <>
onChange={(e) => updateUrl(index, 'community', e.target.value)} <input
disabled={saving || processing} type="text"
/> className={`form-control${invalid ? ' is-invalid' : ''}`}
placeholder="555"
value={url.community}
onChange={(e) => updateUrl(index, 'community', e.target.value)}
disabled={saving || processing}
/>
{invalid && (
<div className="invalid-feedback">Только цифры, например 555</div>
)}
</>
);
})()}
</td> </td>
<td> <td className="text-end">
<button <button
className="btn btn-outline-danger btn-sm" className="btn btn-outline-danger btn-sm"
onClick={() => removeUrl(index)} onClick={() => removeUrl(index)}
@@ -202,32 +271,43 @@ function AutoUrlManager() {
<div className="d-flex justify-content-between align-items-center"> <div className="d-flex justify-content-between align-items-center">
<div className="text-muted"> <div className="text-muted">
{urls.length > 0 && ( {urls.length > 0 && (
<span>Всего URL-адресов: {urls.length}</span> <span className="badge bg-blue-lt text-blue">Всего URL-адресов: {urls.length}</span>
)} )}
</div> </div>
<div> <div>
<button
className="btn btn-outline-secondary me-2"
onClick={() => setUrls(urls.filter(u => u.url || u.community))}
disabled={saving || processing || urls.length === 0}
title="Удалить пустые строки"
>
Очистить пустые
</button>
<button <button
className="btn btn-outline-primary me-2" className="btn btn-outline-primary me-2"
onClick={saveUrls} onClick={saveUrls}
disabled={saving || processing || urls.length === 0} disabled={saving || processing || !hasAtLeastOneValidRow}
> >
{saving ? ( {saving ? (
<> <>
<IconLoader className="animate-spin me-1" /> <span className="spinner-border spinner-border-sm me-2" role="status" />
Сохранение... Сохранение...
</> </>
) : ( ) : (
'Сохранить' <>
<IconDeviceFloppy className="me-1" />
Сохранить
</>
)} )}
</button> </button>
<button <button
className="btn btn-primary" className="btn btn-primary"
onClick={processUrls} onClick={processUrls}
disabled={saving || processing || urls.length === 0} disabled={saving || processing || !hasAtLeastOneValidRow}
> >
{processing ? ( {processing ? (
<> <>
<IconLoader className="animate-spin me-1" /> <span className="spinner-border spinner-border-sm me-2" role="status" />
Обработка... Обработка...
</> </>
) : ( ) : (
@@ -244,7 +324,7 @@ function AutoUrlManager() {
<div className="card mt-4"> <div className="card mt-4">
<div className="card-header"> <div className="card-header">
<h3 className="card-title">Информация</h3> <h3 className="card-title"><IconInfoCircle className="me-2" />Информация</h3>
</div> </div>
<div className="card-body"> <div className="card-body">
<div className="row"> <div className="row">