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}
+ )}
+
+
+
+
+ ))}
+
+
+ );
+}
+
+