Pulse/frontend-modern/src/components/Toast/Toast.tsx

172 lines
5.5 KiB
TypeScript

import { Component, createSignal, For, Show } from 'solid-js';
import { Portal } from 'solid-js/web';
import { POLLING_INTERVALS } from '@/constants';
export type ToastType = 'success' | 'error' | 'warning' | 'info';
export interface ToastMessage {
id: string;
type: ToastType;
title: string;
message?: string;
duration?: number;
}
interface ToastProps {
toast: ToastMessage;
onRemove: (id: string) => void;
}
const Toast: Component<ToastProps> = (props) => {
const [show, setShow] = createSignal(true);
const icons = {
success: (
<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>
),
error: (
<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>
),
warning: (
<div class="relative">
<div class="absolute inset-0 bg-yellow-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 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
/>
</svg>
</div>
),
info: (
<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>
),
};
const iconColors = {
success: 'text-green-400',
error: 'text-red-400',
warning: 'text-yellow-400',
info: 'text-blue-400',
};
const handleClose = () => {
setShow(false);
window.setTimeout(() => props.onRemove(props.toast.id), 300);
};
// Auto-remove after duration
window.setTimeout(() => {
handleClose();
}, props.toast.duration || POLLING_INTERVALS.TOAST_DURATION);
return (
<div
class={`transform transition-all duration-500 ease-out ${
show() ? 'translate-x-0 opacity-100 scale-100' : 'translate-x-full opacity-0 scale-95'
} animate-slide-in-glass`}
>
<div
class={`
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]
`}
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={`flex-shrink-0 ${iconColors[props.toast.type]}`}>{icons[props.toast.type]}</div>
<div class="flex-1">
<h3 class="text-sm font-medium text-gray-800 dark:text-gray-100">{props.toast.title}</h3>
<Show when={props.toast.message}>
<p class="mt-1 text-xs text-gray-700 dark:text-gray-300 opacity-90">
{props.toast.message}
</p>
</Show>
</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>
</div>
);
};
// Toast Container Component
// Declare global interface extension
declare global {
interface Window {
showToast: (type: ToastType, title: string, message?: string, duration?: number) => void;
}
}
export const ToastContainer: Component = () => {
const [toasts, setToasts] = createSignal<ToastMessage[]>([]);
const removeToast = (id: string) => {
setToasts(toasts().filter((t) => t.id !== id));
};
// Expose global toast function
window.showToast = (type: ToastType, title: string, message?: string, duration?: number) => {
const id = Date.now().toString();
setToasts([...toasts(), { id, type, title, message, duration }]);
};
return (
<Portal>
<div class="fixed bottom-4 right-4 z-[9999] space-y-2 max-w-sm">
<For each={toasts()}>{(toast) => <Toast toast={toast} onRemove={removeToast} />}</For>
</div>
</Portal>
);
};