Files
Developer 0a9588abb7 feat: implement QuestionBank CRUD with pagination and template query
- Add pagination support to findAll (page, limit query params)
- Add findByTemplateId method to service
- Add GET /by-template/:templateId endpoint to controller
- Service already includes CRUD for QuestionBank and QuestionBankItem
2026-04-23 17:19:11 +08:00

86 lines
2.5 KiB
TypeScript

import React, { useEffect, useState } from 'react';
import { CheckCircle, AlertCircle, XCircle, Info, X } from 'lucide-react';
export type ToastType = 'success' | 'error' | 'warning' | 'info';
export interface ToastProps {
type: ToastType;
title?: string;
message: string;
duration?: number;
onClose: () => void;
}
const Toast: React.FC<ToastProps> = ({ type, title, message, duration = 5000, onClose }) => {
const [isVisible, setIsVisible] = useState(true);
useEffect(() => {
const timer = setTimeout(() => {
setIsVisible(false);
setTimeout(onClose, 300); // Wait for animation to complete
}, duration);
return () => clearTimeout(timer);
}, [duration, onClose]);
const getIcon = () => {
switch (type) {
case 'success':
return <CheckCircle className="w-5 h-5 text-green-500" />;
case 'error':
return <XCircle className="w-5 h-5 text-red-500" />;
case 'warning':
return <AlertCircle className="w-5 h-5 text-yellow-500" />;
case 'info':
return <Info className="w-5 h-5 text-blue-500" />;
}
};
const getStyles = () => {
switch (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';
}
};
return (
<div
className={`relative w-full transition-all duration-300 ease-in-out ${isVisible ? 'translate-x-0 opacity-100 max-h-40 mb-3' : 'translate-x-full opacity-0 max-h-0 mb-0 overflow-hidden'
}`}
role="alert"
aria-live="polite"
>
<div className={`rounded-lg border shadow-lg p-4 ${getStyles()}`}>
<div className="flex items-start gap-3">
<div className="flex-shrink-0 mt-0.5">
{getIcon()}
</div>
<div className="flex-1 min-w-0">
{title && (
<p className="text-sm font-semibold mb-1">{title}</p>
)}
<p className="text-sm break-words">{message}</p>
</div>
<button
onClick={() => {
setIsVisible(false);
setTimeout(onClose, 300);
}}
className="flex-shrink-0 ml-2 text-gray-400 hover:text-gray-600 transition-colors"
aria-label="Close"
>
<X className="w-4 h-4" />
</button>
</div>
</div>
</div>
);
};
export default Toast;