refactor: unify notifications into global toast
This commit is contained in:
parent
f83caf8933
commit
9d1b9a78c4
6 changed files with 32 additions and 210 deletions
|
|
@ -22,7 +22,6 @@ import { Alerts } from './pages/Alerts';
|
|||
import { DockerHosts } from './components/Docker/DockerHosts';
|
||||
import { ToastContainer } from './components/Toast/Toast';
|
||||
import { ErrorBoundary } from './components/ErrorBoundary';
|
||||
import NotificationContainer from './components/NotificationContainer';
|
||||
import { SecurityWarning } from './components/SecurityWarning';
|
||||
import { Login } from './components/Login';
|
||||
import { logger } from './utils/logger';
|
||||
|
|
@ -585,7 +584,6 @@ function App() {
|
|||
</AppLayout>
|
||||
</div>
|
||||
<ToastContainer />
|
||||
<NotificationContainer />
|
||||
<TokenRevealDialog />
|
||||
<TooltipRoot />
|
||||
</DarkModeContext.Provider>
|
||||
|
|
|
|||
|
|
@ -1,25 +0,0 @@
|
|||
import { Component, For } from 'solid-js';
|
||||
import { Portal } from 'solid-js/web';
|
||||
import NotificationToast from './NotificationToast';
|
||||
import { notificationStore } from '../stores/notifications';
|
||||
|
||||
const NotificationContainer: Component = () => {
|
||||
return (
|
||||
<Portal>
|
||||
<div class="fixed top-4 right-4 z-50 space-y-2">
|
||||
<For each={notificationStore.notifications}>
|
||||
{(notification) => (
|
||||
<NotificationToast
|
||||
message={notification.message}
|
||||
type={notification.type}
|
||||
duration={notification.duration}
|
||||
onClose={() => notificationStore.remove(notification.id)}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotificationContainer;
|
||||
|
|
@ -1,136 +0,0 @@
|
|||
import { Component, createSignal, onCleanup, onMount, Show } from 'solid-js';
|
||||
|
||||
interface NotificationToastProps {
|
||||
message: string;
|
||||
type: 'success' | 'error' | 'info';
|
||||
duration?: number;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
const NotificationToast: Component<NotificationToastProps> = (props) => {
|
||||
const [isVisible, setIsVisible] = createSignal(true);
|
||||
const [isLeaving, setIsLeaving] = createSignal(false);
|
||||
|
||||
const handleClose = () => {
|
||||
setIsLeaving(true);
|
||||
setTimeout(() => {
|
||||
setIsVisible(false);
|
||||
props.onClose?.();
|
||||
}, 300);
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
if (props.duration && props.duration > 0) {
|
||||
const timer = setTimeout(() => {
|
||||
handleClose();
|
||||
}, props.duration);
|
||||
|
||||
onCleanup(() => clearTimeout(timer));
|
||||
}
|
||||
});
|
||||
|
||||
const iconColor = () => {
|
||||
switch (props.type) {
|
||||
case 'success':
|
||||
return 'text-green-400';
|
||||
case 'error':
|
||||
return 'text-red-400';
|
||||
default:
|
||||
return 'text-blue-400';
|
||||
}
|
||||
};
|
||||
|
||||
const icon = () => {
|
||||
switch (props.type) {
|
||||
case 'success':
|
||||
return (
|
||||
<div class="relative">
|
||||
<div class="absolute inset-0 bg-green-400 rounded-full blur-xl opacity-50 animate-pulse"></div>
|
||||
<svg class="w-6 h-6 relative" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2.5"
|
||||
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
case 'error':
|
||||
return (
|
||||
<div class="relative">
|
||||
<div class="absolute inset-0 bg-red-400 rounded-full blur-xl opacity-50 animate-pulse"></div>
|
||||
<svg class="w-6 h-6 relative" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2.5"
|
||||
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<div class="relative">
|
||||
<div class="absolute inset-0 bg-blue-400 rounded-full blur-xl opacity-50 animate-pulse"></div>
|
||||
<svg class="w-6 h-6 relative" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2.5"
|
||||
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Show when={isVisible()}>
|
||||
<div
|
||||
class={`
|
||||
fixed top-4 right-4 z-50
|
||||
backdrop-blur-xl bg-white/10 dark:bg-gray-900/30
|
||||
border border-white/20 dark:border-gray-700/30
|
||||
px-5 py-4 rounded-2xl shadow-2xl
|
||||
flex items-center gap-4
|
||||
min-w-[320px] max-w-[500px]
|
||||
transition-all duration-500 ease-out
|
||||
${isLeaving() ? 'opacity-0 translate-x-full scale-95' : 'opacity-100 translate-x-0 scale-100'}
|
||||
animate-slide-in-glass
|
||||
`}
|
||||
style={{
|
||||
background:
|
||||
'linear-gradient(135deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.05) 100%)',
|
||||
'box-shadow':
|
||||
'0 8px 32px 0 rgba(31, 38, 135, 0.37), inset 0 0 0 1px rgba(255,255,255,0.1)',
|
||||
}}
|
||||
>
|
||||
<div class={`${iconColor()} flex-shrink-0`}>{icon()}</div>
|
||||
<div class="flex-1">
|
||||
<p class="text-gray-800 dark:text-gray-100 font-medium text-sm leading-relaxed">
|
||||
{props.message}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClose}
|
||||
class="flex-shrink-0 text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 hover:bg-white/10 rounded-lg p-1.5 transition-all duration-200"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</Show>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotificationToast;
|
||||
|
|
@ -145,7 +145,7 @@ const Toast: Component<ToastProps> = (props) => {
|
|||
// Declare global interface extension
|
||||
declare global {
|
||||
interface Window {
|
||||
showToast: (type: ToastType, title: string, message?: string, duration?: number) => void;
|
||||
showToast: (type: ToastType, title: string, message?: string, duration?: number) => string;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -158,8 +158,9 @@ export const ToastContainer: Component = () => {
|
|||
|
||||
// Expose global toast function
|
||||
window.showToast = (type: ToastType, title: string, message?: string, duration?: number) => {
|
||||
const id = Date.now().toString();
|
||||
const id = (typeof crypto !== 'undefined' && crypto.randomUUID ? crypto.randomUUID() : Date.now().toString());
|
||||
setToasts([...toasts(), { id, type, title, message, duration }]);
|
||||
return id;
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,44 +1,21 @@
|
|||
import { createStore } from 'solid-js/store';
|
||||
import { showToast } from '@/utils/toast';
|
||||
|
||||
export interface Notification {
|
||||
id: string;
|
||||
message: string;
|
||||
type: 'success' | 'error' | 'info';
|
||||
duration?: number;
|
||||
}
|
||||
|
||||
const [notifications, setNotifications] = createStore<Notification[]>([]);
|
||||
const DEFAULT_DURATIONS: Record<'success' | 'error' | 'info', number> = {
|
||||
success: 5000,
|
||||
error: 10000,
|
||||
info: 5000,
|
||||
};
|
||||
|
||||
export const notificationStore = {
|
||||
notifications,
|
||||
|
||||
add: (notification: Omit<Notification, 'id'>) => {
|
||||
const id = Date.now().toString();
|
||||
setNotifications((prev) => [...prev, { ...notification, id }]);
|
||||
|
||||
// Auto-remove after duration
|
||||
if (notification.duration && notification.duration > 0) {
|
||||
setTimeout(() => {
|
||||
notificationStore.remove(id);
|
||||
}, notification.duration);
|
||||
}
|
||||
|
||||
return id;
|
||||
success: (message: string, duration: number = DEFAULT_DURATIONS.success) => {
|
||||
return showToast('success', message, undefined, duration);
|
||||
},
|
||||
|
||||
remove: (id: string) => {
|
||||
setNotifications((prev) => prev.filter((n) => n.id !== id));
|
||||
error: (message: string, duration: number = DEFAULT_DURATIONS.error) => {
|
||||
return showToast('error', message, undefined, duration);
|
||||
},
|
||||
|
||||
success: (message: string, duration = 5000) => {
|
||||
return notificationStore.add({ message, type: 'success', duration });
|
||||
},
|
||||
|
||||
error: (message: string, duration = 10000) => {
|
||||
return notificationStore.add({ message, type: 'error', duration });
|
||||
},
|
||||
|
||||
info: (message: string, duration = 5000) => {
|
||||
return notificationStore.add({ message, type: 'info', duration });
|
||||
info: (message: string, duration: number = DEFAULT_DURATIONS.info) => {
|
||||
return showToast('info', message, undefined, duration);
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,18 +2,25 @@ import type { ToastType } from '@/components/Toast/Toast';
|
|||
|
||||
// Global declaration is in Toast.tsx
|
||||
|
||||
export const showToast = (type: ToastType, title: string, message?: string, duration?: number) => {
|
||||
// Use the global toast function exposed by ToastContainer
|
||||
export const showToast = (
|
||||
type: ToastType,
|
||||
title: string,
|
||||
message?: string,
|
||||
duration?: number,
|
||||
): string | undefined => {
|
||||
if (typeof window !== 'undefined' && window.showToast) {
|
||||
window.showToast(type, title, message, duration);
|
||||
} else {
|
||||
// Fallback to console if toast system not ready
|
||||
console.log(`[${type.toUpperCase()}] ${title}${message ? ': ' + message : ''}`);
|
||||
return window.showToast(type, title, message, duration);
|
||||
}
|
||||
|
||||
// Fallback to console if toast system not ready
|
||||
console.log(`[${type.toUpperCase()}] ${title}${message ? `: ${message}` : ''}`);
|
||||
return undefined;
|
||||
};
|
||||
|
||||
// Convenience functions - only export what's used
|
||||
export const showSuccess = (title: string, message?: string) =>
|
||||
showToast('success', title, message);
|
||||
export const showError = (title: string, message?: string) => showToast('error', title, message);
|
||||
export const showInfo = (title: string, message?: string) => showToast('info', title, message);
|
||||
export const showSuccess = (title: string, message?: string, duration?: number) =>
|
||||
showToast('success', title, message, duration);
|
||||
export const showError = (title: string, message?: string, duration?: number) =>
|
||||
showToast('error', title, message, duration);
|
||||
export const showInfo = (title: string, message?: string, duration?: number) =>
|
||||
showToast('info', title, message, duration);
|
||||
|
|
|
|||
Loading…
Reference in a new issue