Download frontend/src/components/common/Toast.tsx from fyp-2-skillsync/skillsync-cli: direct link, hf CLI and curl.
- Browser
- Download file 3.4 kB
-
https://huggingface.co/spaces/fyp-2-skillsync/skillsync-cli/resolve/main/frontend/src/components/common/Toast.tsx
- Command line
-
hf download hf://spaces/fyp-2-skillsync/skillsync-cli/frontend/src/components/common/Toast.tsx
-
curl -L -o Toast.tsx https://huggingface.co/spaces/fyp-2-skillsync/skillsync-cli/resolve/main/frontend/src/components/common/Toast.tsx
3.4 kB
| import React from 'react'; | |
| import { motion, AnimatePresence } from 'framer-motion'; | |
| import { CheckCircle, AlertCircle, Info, X } from 'lucide-react'; | |
| import { cn } from '@/utils/helpers'; | |
| export type ToastType = 'success' | 'error' | 'warning' | 'info'; | |
| interface ToastProps { | |
| type: ToastType; | |
| message: string; | |
| title?: string; | |
| onClose?: () => void; | |
| autoClose?: boolean; | |
| autoCloseDuration?: number; | |
| action?: { | |
| label: string; | |
| onClick: () => void; | |
| }; | |
| } | |
| const Toast = React.forwardRef<HTMLDivElement, ToastProps>( | |
| ( | |
| { | |
| type, | |
| message, | |
| title, | |
| onClose, | |
| autoClose = true, | |
| autoCloseDuration = 5000, | |
| action, | |
| }, | |
| ref | |
| ) => { | |
| const [isVisible, setIsVisible] = React.useState(true); | |
| React.useEffect(() => { | |
| if (!autoClose) return; | |
| const timer = setTimeout(() => { | |
| setIsVisible(false); | |
| onClose?.(); | |
| }, autoCloseDuration); | |
| return () => clearTimeout(timer); | |
| }, [autoClose, autoCloseDuration, onClose]); | |
| const icons = { | |
| success: <CheckCircle className="w-5 h-5" />, | |
| error: <AlertCircle className="w-5 h-5" />, | |
| warning: <AlertCircle className="w-5 h-5" />, | |
| info: <Info className="w-5 h-5" />, | |
| }; | |
| const colors = { | |
| success: 'bg-emerald-500/20 border-emerald-500/30 text-emerald-200', | |
| error: 'bg-red-500/20 border-red-500/30 text-red-200', | |
| warning: 'bg-yellow-500/20 border-yellow-500/30 text-yellow-200', | |
| info: 'bg-blue-500/20 border-blue-500/30 text-blue-200', | |
| }; | |
| const iconColors = { | |
| success: 'text-emerald-400', | |
| error: 'text-red-400', | |
| warning: 'text-yellow-400', | |
| info: 'text-blue-400', | |
| }; | |
| return ( | |
| <AnimatePresence> | |
| {isVisible && ( | |
| <motion.div | |
| ref={ref} | |
| initial={{ opacity: 0, y: 20 }} | |
| animate={{ opacity: 1, y: 0 }} | |
| exit={{ opacity: 0, y: 20 }} | |
| className={cn( | |
| 'flex items-start gap-3 w-full max-w-md px-4 py-3 rounded-lg border', | |
| colors[type] | |
| )} | |
| > | |
| <div className={cn('mt-0.5 flex-shrink-0', iconColors[type])}> | |
| {icons[type]} | |
| </div> | |
| <div className="flex-1 min-w-0"> | |
| {title && ( | |
| <p className="font-semibold text-sm mb-0.5">{title}</p> | |
| )} | |
| <p className="text-sm opacity-90">{message}</p> | |
| {action && ( | |
| <motion.button | |
| whileHover={{ x: 2 }} | |
| onClick={action.onClick} | |
| className="text-xs font-medium mt-2 opacity-80 hover:opacity-100 transition-opacity" | |
| > | |
| {action.label} | |
| </motion.button> | |
| )} | |
| </div> | |
| {onClose && ( | |
| <motion.button | |
| whileHover={{ scale: 1.1 }} | |
| whileTap={{ scale: 0.95 }} | |
| onClick={() => { | |
| setIsVisible(false); | |
| onClose?.(); | |
| }} | |
| className="flex-shrink-0 opacity-50 hover:opacity-100 transition-opacity" | |
| > | |
| <X className="w-4 h-4" /> | |
| </motion.button> | |
| )} | |
| </motion.div> | |
| )} | |
| </AnimatePresence> | |
| ); | |
| } | |
| ); | |
| Toast.displayName = 'Toast'; | |
| export default Toast; | |