Files
router-lists-ui/frontend/src/components/ToastContainer.jsx
T

203 lines
5.3 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useEffect, createContext, useContext } from 'react'
import {
IconCheck,
IconX,
IconAlertTriangle,
IconInfoCircle,
IconRefresh
} from '@tabler/icons-react'
/**
* Контекст для глобального управления toast уведомлениями
*/
const ToastContext = createContext({
success: () => {},
error: () => {},
warning: () => {},
info: () => {},
})
export const useToast = () => useContext(ToastContext)
/**
* Toast уведомление (отдельный элемент)
*/
function Toast({ toast, onClose }) {
const [isLeaving, setIsLeaving] = useState(false)
useEffect(() => {
if (toast.duration && toast.duration > 0) {
const timer = setTimeout(() => {
handleClose()
}, toast.duration)
return () => clearTimeout(timer)
}
}, [toast.duration])
const handleClose = () => {
setIsLeaving(true)
setTimeout(() => onClose(toast.id), 300) // анимация 300ms
}
const getIcon = () => {
switch (toast.type) {
case 'success': return <IconCheck size={20} />
case 'error': return <IconX size={20} />
case 'warning': return <IconAlertTriangle size={20} />
case 'info': return <IconInfoCircle size={20} />
default: return null
}
}
const getColorClass = () => {
switch (toast.type) {
case 'success': return 'alert-success'
case 'error': return 'alert-danger'
case 'warning': return 'alert-warning'
case 'info': return 'alert-info'
default: return 'alert-info'
}
}
return (
<div
className={`alert ${getColorClass()} alert-dismissible fade ${isLeaving ? '' : 'show'}`}
role="alert"
style={{
minWidth: '300px',
maxWidth: '400px',
boxShadow: '0 0.5rem 1rem rgba(0,0,0,.15)',
animation: isLeaving ? 'slideOutRight 0.3s ease-out' : 'slideInRight 0.3s ease-out'
}}
>
<div className="d-flex align-items-start">
<div className="me-2 mt-1">
{getIcon()}
</div>
<div className="flex-grow-1">
{toast.title && (
<h4 className="alert-title">{toast.title}</h4>
)}
<div className="text-secondary">{toast.message}</div>
{/* Действия */}
{toast.actions && toast.actions.length > 0 && (
<div className="btn-list mt-2">
{toast.actions.map((action, idx) => (
<button
key={idx}
className={`btn btn-sm ${action.variant || 'btn-outline-primary'}`}
onClick={() => {
action.onClick?.()
handleClose()
}}
>
{action.icon && <action.icon size={14} className="me-1" />}
{action.label}
</button>
))}
</div>
)}
</div>
<button
type="button"
className="btn-close"
onClick={handleClose}
aria-label="Close"
></button>
</div>
</div>
)
}
/**
* Контейнер для всех toast уведомлений
*/
export function ToastContainer({ children }) {
const [toasts, setToasts] = useState([])
const addToast = (type, message, options = {}) => {
const id = Date.now()
const toast = {
id,
type,
message,
title: options.title,
duration: options.duration !== undefined ? options.duration : (type === 'error' ? 0 :
type === 'warning' ? 5000 :
type === 'info' ? 4000 :
3000),
actions: options.actions
}
setToasts(prev => [...prev, toast])
return id
}
const removeToast = (id) => {
setToasts(prev => prev.filter(t => t.id !== id))
}
const contextValue = {
success: (message, options) => addToast('success', message, options),
error: (message, options) => addToast('error', message, options),
warning: (message, options) => addToast('warning', message, options),
info: (message, options) => addToast('info', message, options),
}
// Глобальный доступ через window.toast (для обратной совместимости)
useEffect(() => {
window.toast = contextValue
}, [])
return (
<ToastContext.Provider value={contextValue}>
{children}
{/* Контейнер с toast'ами */}
<div
style={{
position: 'fixed',
bottom: '20px',
right: '20px',
zIndex: 9999,
display: 'flex',
flexDirection: 'column',
gap: '10px'
}}
>
{toasts.map(toast => (
<Toast key={toast.id} toast={toast} onClose={removeToast} />
))}
</div>
{/* CSS анимации */}
<style>{`
@keyframes slideInRight {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes slideOutRight {
from {
transform: translateX(0);
opacity: 1;
}
to {
transform: translateX(100%);
opacity: 0;
}
}
`}</style>
</ToastContext.Provider>
)
}
export default ToastContainer