Files
protoc-frontend/src/component/ui/toast.tsx
2025-07-22 08:34:05 +03:00

177 lines
5.1 KiB
TypeScript
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 { X } from 'lucide-react'
import { FC, useEffect, useState } from 'react'
export type ToastType = 'success' | 'error' | 'warning' | 'info'
export interface Toast {
id: string
type: ToastType
title?: string
message: string
duration?: number
}
interface ToastProps {
toast: Toast
onRemove: (id: string) => void
}
const ToastComponent: FC<ToastProps> = ({ toast, onRemove }) => {
const [isVisible, setIsVisible] = useState(false)
const [isRemoving, setIsRemoving] = useState(false)
useEffect(() => {
// Показываем уведомление с анимацией
setIsVisible(true)
// Автоматическое скрытие
const duration = toast.duration || 5000
const timer = setTimeout(() => {
handleRemove()
}, duration)
return () => clearTimeout(timer)
}, [])
const handleRemove = () => {
setIsRemoving(true)
setTimeout(() => {
onRemove(toast.id)
}, 300) // Время анимации
}
const getTypeStyles = () => {
switch (toast.type) {
case 'success':
return 'bg-green-50 border-green-200 text-green-800'
case 'error':
return 'bg-red-50 border-red-200 text-red-800'
case 'warning':
return 'bg-yellow-50 border-yellow-200 text-yellow-800'
case 'info':
return 'bg-blue-50 border-blue-200 text-blue-800'
default:
return 'bg-gray-50 border-gray-200 text-gray-800'
}
}
const getIconColor = () => {
switch (toast.type) {
case 'success':
return 'text-green-600'
case 'error':
return 'text-red-600'
case 'warning':
return 'text-yellow-600'
case 'info':
return 'text-blue-600'
default:
return 'text-gray-600'
}
}
const getIcon = () => {
switch (toast.type) {
case 'success':
return (
<svg className="h-5 w-5" fill="currentColor" viewBox="0 0 20 20">
<path
fillRule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
clipRule="evenodd"
/>
</svg>
)
case 'error':
return (
<svg className="h-5 w-5" fill="currentColor" viewBox="0 0 20 20">
<path
fillRule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
clipRule="evenodd"
/>
</svg>
)
case 'warning':
return (
<svg className="h-5 w-5" fill="currentColor" viewBox="0 0 20 20">
<path
fillRule="evenodd"
d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z"
clipRule="evenodd"
/>
</svg>
)
case 'info':
return (
<svg className="h-5 w-5" fill="currentColor" viewBox="0 0 20 20">
<path
fillRule="evenodd"
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z"
clipRule="evenodd"
/>
</svg>
)
}
}
return (
<div
className={`
mb-3 transform rounded-lg border p-4 shadow-lg transition-all duration-300 ease-in-out
${getTypeStyles()}
${
isVisible && !isRemoving
? 'translate-x-0 opacity-100'
: '-translate-x-full opacity-0'
}
`}
>
<div className="flex items-start">
<div className={`flex-shrink-0 ${getIconColor()}`}>{getIcon()}</div>
<div className="ml-3 flex-1">
{toast.title && (
<h3 className="text-sm font-medium">{toast.title}</h3>
)}
<div className={`text-sm ${toast.title ? 'mt-1' : ''}`}>
{toast.message}
</div>
</div>
<div className="ml-4 flex flex-shrink-0">
<button
onClick={handleRemove}
className={`
inline-flex rounded-md p-1.5 transition-colors
${getIconColor()} hover:bg-black/10 focus:outline-none focus:ring-2 focus:ring-black/20
`}
>
<X className="h-4 w-4" />
</button>
</div>
</div>
</div>
)
}
interface ToastContainerProps {
toasts: Toast[]
onRemove: (id: string) => void
}
export const ToastContainer: FC<ToastContainerProps> = ({
toasts,
onRemove,
}) => {
if (toasts.length === 0) return null
return (
<div className="pointer-events-none fixed bottom-4 left-4 z-50 flex w-full max-w-sm flex-col-reverse">
{toasts.map(toast => (
<div key={toast.id} className="pointer-events-auto">
<ToastComponent toast={toast} onRemove={onRemove} />
</div>
))}
</div>
)
}