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
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 9m16s
This commit is contained in:
+118
-38
@@ -6,7 +6,10 @@ import {
|
||||
IconDownload,
|
||||
IconAlertCircle,
|
||||
IconCheck,
|
||||
IconLoader
|
||||
IconLoader,
|
||||
IconUpload,
|
||||
IconDeviceFloppy,
|
||||
IconInfoCircle
|
||||
} from '@tabler/icons-react';
|
||||
|
||||
function AutoUrlManager() {
|
||||
@@ -16,6 +19,7 @@ function AutoUrlManager() {
|
||||
const [processing, setProcessing] = useState(false);
|
||||
const [message, setMessage] = useState('');
|
||||
const [messageType, setMessageType] = useState('');
|
||||
const [touched, setTouched] = useState({});
|
||||
|
||||
useEffect(() => {
|
||||
fetchUrls();
|
||||
@@ -47,17 +51,40 @@ function AutoUrlManager() {
|
||||
const newUrls = [...urls];
|
||||
newUrls[index][field] = value;
|
||||
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 () => {
|
||||
try {
|
||||
setSaving(true);
|
||||
setMessage('');
|
||||
|
||||
// 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) {
|
||||
setMessage('Добавьте хотя бы один URL с community');
|
||||
setMessage('Добавьте хотя бы одну корректную запись (валидный URL и числовой community)');
|
||||
setMessageType('error');
|
||||
return;
|
||||
}
|
||||
@@ -95,7 +122,7 @@ function AutoUrlManager() {
|
||||
if (loading) {
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@@ -125,14 +152,34 @@ function AutoUrlManager() {
|
||||
<div className="card-header">
|
||||
<h3 className="card-title">URL-адреса для автоматической загрузки</h3>
|
||||
<div className="card-actions">
|
||||
<button
|
||||
className="btn btn-primary btn-sm"
|
||||
onClick={addUrl}
|
||||
disabled={saving || processing}
|
||||
>
|
||||
<IconPlus className="me-1" />
|
||||
Добавить URL
|
||||
</button>
|
||||
<div className="btn-list">
|
||||
<button
|
||||
className="btn btn-outline-secondary btn-sm"
|
||||
onClick={() => {
|
||||
const text = window.prompt('Вставьте строки вида: URL ПРОБЕЛ COMMUNITY (по одной записи на строку)');
|
||||
if (!text) return;
|
||||
const lines = text.split(/\r?\n/).map(l => l.trim()).filter(Boolean);
|
||||
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 className="card-body">
|
||||
@@ -151,38 +198,60 @@ function AutoUrlManager() {
|
||||
</div>
|
||||
) : (
|
||||
<div className="table-responsive">
|
||||
<table className="table table-vcenter">
|
||||
<table className="table card-table table-vcenter table-nowrap mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>URL</th>
|
||||
<th>Community</th>
|
||||
<th width="100">Действия</th>
|
||||
<th className="text-end" style={{ width: 100 }}>Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{urls.map((url, index) => (
|
||||
<tr key={index}>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder="https://example.com/ips.txt"
|
||||
value={url.url}
|
||||
onChange={(e) => updateUrl(index, 'url', e.target.value)}
|
||||
disabled={saving || processing}
|
||||
/>
|
||||
{(() => {
|
||||
const v = getRowValidity(url);
|
||||
const invalid = touched[index] && !v.url;
|
||||
return (
|
||||
<>
|
||||
<input
|
||||
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>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder="555"
|
||||
value={url.community}
|
||||
onChange={(e) => updateUrl(index, 'community', e.target.value)}
|
||||
disabled={saving || processing}
|
||||
/>
|
||||
{(() => {
|
||||
const v = getRowValidity(url);
|
||||
const invalid = touched[index] && !v.community;
|
||||
return (
|
||||
<>
|
||||
<input
|
||||
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 className="text-end">
|
||||
<button
|
||||
className="btn btn-outline-danger btn-sm"
|
||||
onClick={() => removeUrl(index)}
|
||||
@@ -202,32 +271,43 @@ function AutoUrlManager() {
|
||||
<div className="d-flex justify-content-between align-items-center">
|
||||
<div className="text-muted">
|
||||
{urls.length > 0 && (
|
||||
<span>Всего URL-адресов: {urls.length}</span>
|
||||
<span className="badge bg-blue-lt text-blue">Всего URL-адресов: {urls.length}</span>
|
||||
)}
|
||||
</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
|
||||
className="btn btn-outline-primary me-2"
|
||||
onClick={saveUrls}
|
||||
disabled={saving || processing || urls.length === 0}
|
||||
disabled={saving || processing || !hasAtLeastOneValidRow}
|
||||
>
|
||||
{saving ? (
|
||||
<>
|
||||
<IconLoader className="animate-spin me-1" />
|
||||
<span className="spinner-border spinner-border-sm me-2" role="status" />
|
||||
Сохранение...
|
||||
</>
|
||||
) : (
|
||||
'Сохранить'
|
||||
<>
|
||||
<IconDeviceFloppy className="me-1" />
|
||||
Сохранить
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={processUrls}
|
||||
disabled={saving || processing || urls.length === 0}
|
||||
disabled={saving || processing || !hasAtLeastOneValidRow}
|
||||
>
|
||||
{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-header">
|
||||
<h3 className="card-title">Информация</h3>
|
||||
<h3 className="card-title"><IconInfoCircle className="me-2" />Информация</h3>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<div className="row">
|
||||
|
||||
Reference in New Issue
Block a user