diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 04b4440..3492ce8 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -294,35 +294,79 @@ function MainLayout() { - {/* Правая часть навбара: язык и тема */} -
-
- + {lang.toUpperCase()} + +
+ { e.preventDefault(); setLang('ru'); }} + > + Русский + + { e.preventDefault(); setLang('en'); }} + > + English + +
+ + {/* Тема */} +
+ e.preventDefault()} + > + {theme === 'dark' ? '🌙' : '☀️'} + +
+ { e.preventDefault(); setTheme('light'); }} + > + ☀️ {t('light')} + + { e.preventDefault(); setTheme('dark'); }} + > + 🌙 {t('dark')} + +
+
+ + {/* Горячие клавиши */} +
+ +
+ + {/* Настройки */}
- -
-
- - + +
diff --git a/frontend/src/DomainsNewManager.jsx b/frontend/src/DomainsNewManager.jsx index 2c9b835..8c6ecfb 100644 --- a/frontend/src/DomainsNewManager.jsx +++ b/frontend/src/DomainsNewManager.jsx @@ -16,7 +16,8 @@ import { IconDeviceFloppy, IconHash, IconClock, - IconFileText + IconFileText, + IconFilter } from '@tabler/icons-react'; import TableSkeleton, { TableEmpty } from './components/TableSkeleton.jsx'; import PageHeader from './components/PageHeader.jsx'; @@ -33,10 +34,15 @@ import { notifyMutationSuccess } from './components/NotifyProvider.jsx'; import ImportModal from './components/ImportModal.jsx'; import QuickAddBar from './components/QuickAddBar.jsx'; import AccordionCard from './components/AccordionCard.jsx'; +import SavedFilters from './components/SavedFilters.jsx'; +import BulkActionsBar from './components/BulkActionsBar.jsx'; +import ValidatedInput from './components/ValidatedInput.jsx'; +import { useToast } from './components/ToastContainer.jsx'; const API_URL = '/api'; function DomainsNewManager() { + const toast = useToast(); const [items, setItems] = useState([]); const [originalItems, setOriginalItems] = useState([]); const [etag, setEtag] = useState(''); @@ -62,6 +68,9 @@ function DomainsNewManager() { const editInputRef = useRef(null); const pageSize = 10; + // Bulk operations + const [selectedDomains, setSelectedDomains] = useState(new Set()); + // Справочник community для подсказок const [communities, setCommunities] = useState([]); useEffect(() => { @@ -399,6 +408,47 @@ function DomainsNewManager() { if (e.key === 'Escape') handleCancelEdit(); }; + // Bulk operations handlers + const handleSelectAll = () => { + setSelectedDomains(new Set(paginatedItems.map(i => i.domain))); + }; + + const handleDeselectAll = () => { + setSelectedDomains(new Set()); + }; + + const handleToggleSelect = (domain) => { + const newSet = new Set(selectedDomains); + if (newSet.has(domain)) { + newSet.delete(domain); + } else { + newSet.add(domain); + } + setSelectedDomains(newSet); + }; + + const handleBulkDelete = () => { + if (selectedDomains.size === 0) return; + const newItems = items.filter(i => !selectedDomains.has(i.domain)); + setItems(newItems); + setSelectedDomains(new Set()); + toast.success(`Удалено ${selectedDomains.size} доменов`); + }; + + const handleBulkExport = () => { + if (selectedDomains.size === 0) return; + const selectedItems = items.filter(i => selectedDomains.has(i.domain)); + const text = selectedItems.map(i => `${i.domain} ${i.community}`).join('\n'); + const blob = new Blob([text], { type: 'text/plain' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `domains-selected-${selectedDomains.size}.txt`; + a.click(); + URL.revokeObjectURL(url); + toast.success(`Экспортировано ${selectedDomains.size} доменов`); + }; + return ( <> {error && ( @@ -422,11 +472,16 @@ function DomainsNewManager() { + )} actions={(
+ {/* Bulk Actions Bar */} + +
-
-

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

-
- {/* Фильтр по community */} - - {/* Поиск */} -
- - - - { setSearchTerm(e.target.value); setCurrentPage(1); }} - aria-label="Поиск домена" - /> +
+
+
+

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

+
+
+
+ {/* Сохранённые фильтры */} + { + if (filters.searchTerm !== undefined) setSearchTerm(filters.searchTerm); + if (filters.filterCommunity !== undefined) setFilterCommunity(filters.filterCommunity); + if (filters.sortField !== undefined) setSortField(filters.sortField); + if (filters.sortOrder !== undefined) setSortOrder(filters.sortOrder); + setCurrentPage(1); + }} + /> + + {/* Фильтр по community */} + + + {/* Поиск */} +
+ + + + { setSearchTerm(e.target.value); setCurrentPage(1); }} + aria-label="Поиск домена" + /> +
+
-
{/* Мета-информация будет показана внизу карточки */} {loading ? ( - + ) : paginatedItems.length === 0 ? ( + + 0} + onChange={(e) => e.target.checked ? handleSelectAll() : handleDeselectAll()} + /> + handleSort('domain')}> Домен {sortField === 'domain' && ( @@ -654,6 +749,14 @@ function DomainsNewManager() { {paginatedItems.map((item) => ( + + handleToggleSelect(item.domain)} + /> + {item.domain} {renderCommunityBadge(item.community)} diff --git a/frontend/src/components/CommandPalette.jsx b/frontend/src/components/CommandPalette.jsx index ed3b7cb..8ec0669 100644 --- a/frontend/src/components/CommandPalette.jsx +++ b/frontend/src/components/CommandPalette.jsx @@ -203,15 +203,29 @@ export function KeyboardShortcutsButton({ className = '' }) { { keys: ['Enter'], description: 'Подтвердить / Выбрать' }, ] + // Если className содержит nav-link, используем ссылку вместо кнопки + const isNavLink = className.includes('nav-link') + return ( <> - + {isNavLink ? ( + { e.preventDefault(); setShowModal(true); }} + title="Горячие клавиши (Ctrl+/)" + > + + + ) : ( + + )} {showModal && ( <>