diff --git a/frontend/src/ASNsNewManager.jsx b/frontend/src/ASNsNewManager.jsx index f2c4fba..dc6a4d2 100644 --- a/frontend/src/ASNsNewManager.jsx +++ b/frontend/src/ASNsNewManager.jsx @@ -547,7 +547,7 @@ function ASNsNewManager() { - + {/* Мета-информация будет показана внизу карточки */}
@@ -641,21 +641,7 @@ function ASNsNewManager() { )} - {(etag || lastModified || contentLength !== null) && ( -
-
- - {etag || '—'} - - - {lastModified || '—'} - - - {(contentLength ?? '—') + (contentLength !== null ? ' байт' : '')} - -
-
- )} + diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 88d6e95..ede574c 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -22,6 +22,7 @@ import BillingManager from './BillingManager'; import CommunitiesManager from './CommunitiesManager'; import Dashboard from './Dashboard'; import './App.css'; +import { NotifyProvider } from './components/NotifyProvider.jsx'; import axios from 'axios'; // --- Simple i18n (RU/EN) --- @@ -81,7 +82,9 @@ function App() { - + + + diff --git a/frontend/src/DomainsNewManager.jsx b/frontend/src/DomainsNewManager.jsx index ea98bd9..a7f4787 100644 --- a/frontend/src/DomainsNewManager.jsx +++ b/frontend/src/DomainsNewManager.jsx @@ -555,7 +555,7 @@ function DomainsNewManager() { - + {/* Мета-информация будет показана внизу карточки */}
@@ -649,21 +649,7 @@ function DomainsNewManager() { )} - {(etag || lastModified || contentLength !== null) && ( -
-
- - {etag || '—'} - - - {lastModified || '—'} - - - {(contentLength ?? '—') + (contentLength !== null ? ' байт' : '')} - -
-
- )} + diff --git a/frontend/src/IPRangesManager.jsx b/frontend/src/IPRangesManager.jsx index 320d667..fd5d801 100644 --- a/frontend/src/IPRangesManager.jsx +++ b/frontend/src/IPRangesManager.jsx @@ -563,7 +563,7 @@ function IPRangesManager() { - + {/* Мета-информация будет показана внизу карточки */}
@@ -662,21 +662,7 @@ function IPRangesManager() { )} - {(etag || lastModified || contentLength !== null) && ( -
-
- - {etag || '—'} - - - {lastModified || '—'} - - - {(contentLength ?? '—') + (contentLength !== null ? ' байт' : '')} - -
-
- )} + diff --git a/frontend/src/components/NotifyProvider.jsx b/frontend/src/components/NotifyProvider.jsx new file mode 100644 index 0000000..7ab34ce --- /dev/null +++ b/frontend/src/components/NotifyProvider.jsx @@ -0,0 +1,134 @@ +import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'; +import { IconCheck, IconAlertTriangle, IconInfoCircle, IconX } from '@tabler/icons-react'; + +const NotifyContext = createContext({ + add: () => {}, + success: () => {}, + error: () => {}, + info: () => {}, + warning: () => {}, + remove: () => {}, + clear: () => {}, +}); + +const DEFAULT_TIMEOUT = { + success: 3000, + info: 4000, + warning: 5000, + error: 7000, +}; + +export function NotifyProvider({ children }) { + const [items, setItems] = useState([]); // { id, type, message, count, createdAt } + const timeouts = useRef(new Map()); + const idSeq = useRef(1); + + const remove = useCallback((id) => { + setItems((prev) => prev.filter((n) => n.id !== id)); + const t = timeouts.current.get(id); + if (t) { clearTimeout(t); timeouts.current.delete(id); } + }, []); + + const schedule = useCallback((id, type) => { + const dur = DEFAULT_TIMEOUT[type] ?? 4000; + const old = timeouts.current.get(id); + if (old) clearTimeout(old); + const t = setTimeout(() => remove(id), dur); + timeouts.current.set(id, t); + }, [remove]); + + const add = useCallback((type, message) => { + const text = String(message || '').trim(); + if (!text) return; + setItems((prev) => { + const dup = prev.find((n) => n.type === type && n.message === text); + if (dup) { + const updated = prev.map((n) => n.id === dup.id ? { ...n, count: (n.count || 1) + 1, createdAt: Date.now() } : n); + schedule(dup.id, type); + return updated; + } + const id = idSeq.current++; + const next = [...prev, { id, type, message: text, count: 1, createdAt: Date.now() }]; + schedule(id, type); + return next; + }); + }, [schedule]); + + const clear = useCallback(() => { + setItems([]); + for (const t of timeouts.current.values()) clearTimeout(t); + timeouts.current.clear(); + }, []); + + const api = useMemo(() => ({ + add, + success: (m) => add('success', m), + error: (m) => add('error', m), + info: (m) => add('info', m), + warning: (m) => add('warning', m), + remove, + clear, + }), [add, remove, clear]); + + useEffect(() => { + // Опционально доступ из консоли / старого кода + window.notify = api; + return () => { if (window.notify === api) delete window.notify; }; + }, [api]); + + return ( + + {children} + + + ); +} + +export function useNotify() { + return useContext(NotifyContext); +} + +function colorByType(type) { + switch (type) { + case 'success': return 'success'; + case 'error': return 'danger'; + case 'warning': return 'warning'; + default: return 'info'; + } +} + +function iconByType(type) { + switch (type) { + case 'success': return ; + case 'warning': return ; + case 'error': return ; + default: return ; + } +} + +function NotifyViewport({ items, onClose }) { + return ( +
+
+ {items.map((n) => ( +
+
+
{iconByType(n.type)}
+
+ {n.message} + {n.count > 1 && ( + ×{n.count} + )} +
+ +
+
+ ))} +
+
+ ); +} + +