'use client'
import { useState, useEffect } from 'react'
import { IoCheckmarkCircleOutline, IoArrowBackOutline, IoEyeOutline, IoCloseOutline } from 'react-icons/io5'
interface FloatingToastProps {
show: boolean
type?: 'ai-changes' | 'external-changes' | 'success' | 'error'
message?: string
actions?: Array<{
label: string
onClick: () => void
variant?: 'primary' | 'secondary' | 'danger'
loading?: boolean
icon?: React.ReactNode
}>
onDismiss?: () => void
autoHide?: number // milliseconds
position?: 'top-right' | 'bottom-right' | 'top-center' | 'bottom-center'
offset?: number // For stacking multiple toasts
}
export default function FloatingToast({
show,
type = 'ai-changes',
message,
actions = [],
onDismiss,
autoHide,
position = 'top-right',
offset = 0
}: FloatingToastProps) {
const [isVisible, setIsVisible] = useState(false)
const [isAnimating, setIsAnimating] = useState(false)
useEffect(() => {
if (show) {
setIsVisible(true)
setTimeout(() => setIsAnimating(true), 10) // Small delay for smooth entrance
} else {
setIsAnimating(false)
setTimeout(() => setIsVisible(false), 300) // Wait for exit animation
}
}, [show])
useEffect(() => {
if (autoHide && show) {
const timer = setTimeout(() => {
onDismiss?.()
}, autoHide)
return () => clearTimeout(timer)
}
}, [autoHide, show, onDismiss])
if (!isVisible) return null
const getPositionStyles = () => {
const baseOffset = offset * 70 // 70px spacing between toasts
switch (position) {
case 'top-right':
return { top: `${16 + baseOffset}px`, right: '16px' }
case 'bottom-right':
return { bottom: `${16 + baseOffset}px`, right: '16px' }
case 'top-center':
return { top: `${16 + baseOffset}px`, left: '50%', transform: 'translateX(-50%)' }
case 'bottom-center':
return { bottom: `${16 + baseOffset}px`, left: '50%', transform: 'translateX(-50%)' }
default:
return { top: `${16 + baseOffset}px`, right: '16px' }
}
}
const getTypeStyles = () => {
switch (type) {
case 'ai-changes':
return {
bg: 'bg-slate-800/95',
accent: 'border-teal-500/30',
icon: '✨',
iconColor: 'text-teal-400'
}
case 'external-changes':
return {
bg: 'bg-slate-800/95',
accent: 'border-blue-500/30',
icon: '🔄',
iconColor: 'text-blue-400'
}
case 'success':
return {
bg: 'bg-slate-800/95',
accent: 'border-emerald-500/30',
icon: '✓',
iconColor: 'text-emerald-400'
}
case 'error':
return {
bg: 'bg-slate-800/95',
accent: 'border-red-500/30',
icon: '×',
iconColor: 'text-red-400'
}
default:
return {
bg: 'bg-slate-800/95',
accent: 'border-slate-500/30',
icon: 'ℹ',
iconColor: 'text-slate-400'
}
}
}
const typeStyles = getTypeStyles()
const getButtonVariantStyles = (variant?: string) => {
switch (variant) {
case 'primary':
return 'bg-slate-700 hover:bg-slate-600 text-slate-200 border border-slate-600'
case 'danger':
return 'bg-red-500/20 hover:bg-red-500/30 text-red-400 border border-red-500/30'
case 'secondary':
default:
return 'bg-slate-700/50 hover:bg-slate-700 text-slate-300 border border-slate-600/50'
}
}
return (
<div
className={`
fixed z-50
transition-all duration-200 ease-out
${isAnimating
? 'opacity-100 translate-y-0'
: 'opacity-0 translate-y-1'
}
`}
style={getPositionStyles()}
>
<div className={`
${typeStyles.bg} backdrop-blur-md
rounded-lg shadow-lg border ${typeStyles.accent}
p-3 min-w-[260px] max-w-[320px]
text-slate-100 text-xs
`}>
<div className="flex items-center justify-between gap-3">
{/* Icon and Message */}
<div className="flex items-center gap-2.5 flex-1 min-w-0">
<span className={`text-sm flex-shrink-0 font-medium ${typeStyles.iconColor}`}>
{typeStyles.icon}
</span>
<span className="font-medium truncate text-slate-200">
{message || 'Notification'}
</span>
</div>
{/* Dismiss button */}
{onDismiss && (
<button
onClick={onDismiss}
className="flex-shrink-0 p-1 rounded-md hover:bg-slate-700/50 transition-colors text-slate-400 hover:text-slate-300"
>
<IoCloseOutline className="w-3.5 h-3.5" />
</button>
)}
</div>
{/* Actions */}
{actions.length > 0 && (
<div className="flex items-center gap-2 mt-2.5 pt-2 border-t border-slate-700/50">
{actions.map((action, index) => (
<button
key={index}
onClick={action.onClick}
disabled={action.loading}
className={`
${getButtonVariantStyles(action.variant)}
px-2.5 py-1 rounded-md text-xs font-medium
transition-all duration-150
flex items-center gap-1.5
disabled:opacity-50 disabled:cursor-not-allowed
`}
>
{action.loading ? (
<div className="w-3 h-3 border border-current border-t-transparent rounded-full animate-spin opacity-70" />
) : action.icon ? (
action.icon
) : null}
<span>{action.label}</span>
</button>
))}
</div>
)}
</div>
</div>
)
}