Toast Notification System
Overview
PadawanForge implements a comprehensive toast notification system that provides user feedback, error messages, and success confirmations. This system supports multiple notification types, customizable styling, and automatic dismissal with configurable timing.
Architecture
Core Components
- ToastManager: Central notification management
- Toast Types: Success, error, warning, info notifications
- Auto-dismiss: Configurable timeout and dismissal
- Queue Management: Notification queuing and stacking
- Custom Styling: Theme-aware notification styling
Notification Features
- Multiple Types: Success, error, warning, info, loading
- Auto-dismiss: Configurable timeout periods
- Manual Dismiss: User-controlled dismissal
- Queue System: Handle multiple notifications
- Theme Integration: Dark/light mode support
- Accessibility: Screen reader support
Implementation
ToastManager Class
import { ToastManager, ToastType, ToastOptions } from '@/lib/toast';
// Get singleton instance
const toast = ToastManager.getInstance();
// Show different types of notifications
toast.success('Operation completed successfully!');
toast.error('Something went wrong');
toast.warning('Please check your input');
toast.info('New message received');
toast.loading('Processing...');
// Show with custom options
toast.show('Custom message', {
type: 'success',
duration: 5000,
dismissible: true,
action: {
label: 'Undo',
onClick: () => console.log('Undo clicked')
}
});
Toast Interface
interface Toast {
id: string; // Unique toast ID
message: string; // Toast message
type: ToastType; // Toast type
duration?: number; // Auto-dismiss duration
dismissible?: boolean; // Can be manually dismissed
action?: ToastAction; // Action button
createdAt: Date; // Creation timestamp
}
interface ToastOptions {
type?: ToastType; // Toast type
duration?: number; // Auto-dismiss duration
dismissible?: boolean; // Manual dismissal
action?: ToastAction; // Action button
position?: ToastPosition; // Display position
}
interface ToastAction {
label: string; // Action button text
onClick: () => void; // Action handler
}
type ToastType = 'success' | 'error' | 'warning' | 'info' | 'loading';
type ToastPosition = 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left' | 'top-center' | 'bottom-center';
Usage Examples
Basic Notifications
import { toast } from '@/lib/toast';
// Success notifications
toast.success('Profile updated successfully!');
toast.success('Game session completed!', { duration: 3000 });
// Error notifications
toast.error('Failed to save changes');
toast.error('Network connection lost', { dismissible: true });
// Warning notifications
toast.warning('Please complete your profile');
toast.warning('Session will expire soon', { duration: 10000 });
// Info notifications
toast.info('New message from John');
toast.info('System maintenance in 5 minutes');
// Loading notifications
const loadingToast = toast.loading('Processing your request...');
// Later, dismiss or update
loadingToast.dismiss();
// or
toast.success('Request completed!');
Advanced Notifications
// Custom duration
toast.success('Quick success message', { duration: 2000 });
// Non-dismissible notification
toast.error('Critical error - please contact support', {
dismissible: false,
duration: 0 // Never auto-dismiss
});
// With action button
toast.error('Failed to upload file', {
action: {
label: 'Retry',
onClick: () => retryUpload()
}
});
// Custom position
toast.info('New achievement unlocked!', {
position: 'top-center',
duration: 5000
});
Toast with Actions
// Undo action
toast.success('Item deleted', {
action: {
label: 'Undo',
onClick: () => {
undoDelete();
toast.success('Item restored');
}
}
});
// Retry action
toast.error('Connection failed', {
action: {
label: 'Retry',
onClick: async () => {
try {
await reconnect();
toast.success('Reconnected successfully');
} catch (error) {
toast.error('Reconnection failed');
}
}
}
});
// View details action
toast.info('New notification received', {
action: {
label: 'View',
onClick: () => {
openNotifications();
toast.dismiss();
}
}
});
Toast Types and Styling
Success Notifications
// Success toast styling
const successToast = {
backgroundColor: 'bg-green-500',
textColor: 'text-white',
icon: 'β',
borderColor: 'border-green-600'
};
// Usage
toast.success('Operation completed successfully!', {
duration: 3000,
dismissible: true
});
Error Notifications
// Error toast styling
const errorToast = {
backgroundColor: 'bg-red-500',
textColor: 'text-white',
icon: 'β',
borderColor: 'border-red-600'
};
// Usage
toast.error('An error occurred while processing your request', {
duration: 5000,
dismissible: true,
action: {
label: 'Report Issue',
onClick: () => reportIssue()
}
});
Warning Notifications
// Warning toast styling
const warningToast = {
backgroundColor: 'bg-yellow-500',
textColor: 'text-white',
icon: 'β ',
borderColor: 'border-yellow-600'
};
// Usage
toast.warning('Your session will expire in 5 minutes', {
duration: 10000,
dismissible: true
});
Info Notifications
// Info toast styling
const infoToast = {
backgroundColor: 'bg-blue-500',
textColor: 'text-white',
icon: 'βΉ',
borderColor: 'border-blue-600'
};
// Usage
toast.info('New features available! Check them out.', {
duration: 4000,
dismissible: true
});
Loading Notifications
// Loading toast styling
const loadingToast = {
backgroundColor: 'bg-gray-500',
textColor: 'text-white',
icon: 'β³',
borderColor: 'border-gray-600',
animated: true
};
// Usage
const loading = toast.loading('Processing your request...');
// Later, dismiss or replace
setTimeout(() => {
loading.dismiss();
toast.success('Request completed!');
}, 3000);
Queue Management
Multiple Notifications
// Handle multiple notifications
class NotificationQueue {
private queue: Toast[] = [];
private maxVisible = 3;
addToast(toast: Toast) {
this.queue.push(toast);
this.updateDisplay();
}
removeToast(toastId: string) {
this.queue = this.queue.filter(t => t.id !== toastId);
this.updateDisplay();
}
private updateDisplay() {
const visibleToasts = this.queue.slice(0, this.maxVisible);
// Update UI to show visible toasts
}
}
// Usage
toast.success('First notification');
toast.info('Second notification');
toast.warning('Third notification');
toast.error('Fourth notification'); // Will be queued
Toast Stacking
// Stack similar notifications
class ToastStacker {
private stacks = new Map<string, Toast[]>();
addToast(toast: Toast, stackKey?: string) {
if (!stackKey) {
this.showToast(toast);
return;
}
const stack = this.stacks.get(stackKey) || [];
stack.push(toast);
this.stacks.set(stackKey, stack);
if (stack.length === 1) {
this.showToast(toast);
} else {
this.updateStackDisplay(stackKey);
}
}
private updateStackDisplay(stackKey: string) {
const stack = this.stacks.get(stackKey) || [];
const count = stack.length;
// Update the displayed toast to show count
const displayToast = {
...stack[0],
message: `${stack[0].message} (${count} more)`
};
this.showToast(displayToast);
}
}
// Usage
toast.success('Message sent', { stackKey: 'messages' });
toast.success('Message sent', { stackKey: 'messages' });
toast.success('Message sent', { stackKey: 'messages' });
// Shows: "Message sent (3 more)"
Theme Integration
Dark Mode Support
// Theme-aware toast styling
const getToastStyles = (type: ToastType, isDark: boolean) => {
const baseStyles = {
success: {
light: { bg: 'bg-green-500', text: 'text-white', border: 'border-green-600' },
dark: { bg: 'bg-green-600', text: 'text-white', border: 'border-green-700' }
},
error: {
light: { bg: 'bg-red-500', text: 'text-white', border: 'border-red-600' },
dark: { bg: 'bg-red-600', text: 'text-white', border: 'border-red-700' }
},
warning: {
light: { bg: 'bg-yellow-500', text: 'text-white', border: 'border-yellow-600' },
dark: { bg: 'bg-yellow-600', text: 'text-white', border: 'border-yellow-700' }
},
info: {
light: { bg: 'bg-blue-500', text: 'text-white', border: 'border-blue-600' },
dark: { bg: 'bg-blue-600', text: 'text-white', border: 'border-blue-700' }
}
};
const theme = isDark ? 'dark' : 'light';
return baseStyles[type][theme];
};
// Usage
const isDarkMode = useTheme() === 'dark';
const styles = getToastStyles('success', isDarkMode);
Responsive Design
// Responsive toast positioning
const getToastPosition = (position: ToastPosition, isMobile: boolean) => {
if (isMobile) {
// On mobile, always use bottom positioning
return 'bottom-center';
}
return position;
};
// Usage
const isMobile = window.innerWidth < 768;
const position = getToastPosition('top-right', isMobile);
Accessibility Features
Screen Reader Support
// Accessible toast implementation
function createAccessibleToast(toast: Toast) {
return {
role: 'alert',
'aria-live': 'polite',
'aria-atomic': 'true',
'aria-label': `${toast.type} notification: ${toast.message}`,
tabIndex: 0,
onKeyDown: (e: KeyboardEvent) => {
if (e.key === 'Escape') {
toast.dismiss();
}
}
};
}
// Usage
const accessibleProps = createAccessibleToast(toast);
Keyboard Navigation
// Keyboard shortcuts for toast management
class ToastKeyboardManager {
constructor() {
this.setupKeyboardListeners();
}
private setupKeyboardListeners() {
document.addEventListener('keydown', (e) => {
// Escape to dismiss all toasts
if (e.key === 'Escape') {
toast.dismissAll();
}
// Ctrl/Cmd + Shift + T to show toast test
if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 'T') {
toast.info('Toast test notification');
}
});
}
}
Integration Examples
API Error Handling
// Handle API errors with toast notifications
async function handleApiCall(apiFunction: () => Promise<any>) {
const loadingToast = toast.loading('Processing request...');
try {
const result = await apiFunction();
loadingToast.dismiss();
toast.success('Request completed successfully!');
return result;
} catch (error) {
loadingToast.dismiss();
if (error.status === 401) {
toast.error('Please log in to continue');
} else if (error.status === 403) {
toast.error('You do not have permission to perform this action');
} else if (error.status === 404) {
toast.error('Resource not found');
} else if (error.status >= 500) {
toast.error('Server error. Please try again later.');
} else {
toast.error(error.message || 'An unexpected error occurred');
}
throw error;
}
}
// Usage
const result = await handleApiCall(() => api.createUser(userData));
Form Validation
// Form validation with toast feedback
function validateForm(formData: any) {
const errors: string[] = [];
if (!formData.username) {
errors.push('Username is required');
}
if (!formData.email) {
errors.push('Email is required');
} else if (!isValidEmail(formData.email)) {
errors.push('Please enter a valid email address');
}
if (formData.password && formData.password.length < 8) {
errors.push('Password must be at least 8 characters long');
}
return errors;
}
// Usage in form submission
async function handleFormSubmit(formData: any) {
const errors = validateForm(formData);
if (errors.length > 0) {
errors.forEach(error => toast.error(error));
return;
}
const loadingToast = toast.loading('Saving changes...');
try {
await saveFormData(formData);
loadingToast.dismiss();
toast.success('Changes saved successfully!');
} catch (error) {
loadingToast.dismiss();
toast.error('Failed to save changes. Please try again.');
}
}
Game Notifications
// Game-specific notifications
class GameNotificationManager {
// Level up notification
showLevelUp(oldLevel: number, newLevel: number) {
toast.success(`π Level Up! You reached level ${newLevel}!`, {
duration: 5000,
action: {
label: 'View Stats',
onClick: () => openStats()
}
});
}
// Achievement unlocked
showAchievement(achievement: any) {
toast.success(`π Achievement Unlocked: ${achievement.name}`, {
duration: 6000,
action: {
label: 'View',
onClick: () => openAchievements()
}
});
}
// Game session complete
showSessionComplete(score: number, accuracy: number) {
toast.success(`Game Complete! Score: ${score}, Accuracy: ${accuracy}%`, {
duration: 4000,
action: {
label: 'Play Again',
onClick: () => startNewGame()
}
});
}
// Connection lost
showConnectionLost() {
toast.error('Connection lost. Attempting to reconnect...', {
duration: 0,
dismissible: false
});
}
// Reconnected
showReconnected() {
toast.success('Reconnected successfully!', {
duration: 3000
});
}
}
// Usage
const gameNotifications = new GameNotificationManager();
gameNotifications.showLevelUp(5, 6);
Component Integration
React Toast Component
import { useState, useEffect } from 'react';
import { toast, ToastManager } from '@/lib/toast';
function ToastContainer() {
const [toasts, setToasts] = useState<Toast[]>([]);
useEffect(() => {
const toastManager = ToastManager.getInstance();
const unsubscribe = toastManager.subscribe((newToasts) => {
setToasts(newToasts);
});
return unsubscribe;
}, []);
return (
<div className="toast-container">
{toasts.map(toast => (
<ToastItem key={toast.id} toast={toast} />
))}
</div>
);
}
function ToastItem({ toast }: { toast: Toast }) {
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
setIsVisible(true);
if (toast.duration && toast.duration > 0) {
const timer = setTimeout(() => {
setIsVisible(false);
setTimeout(() => toast.dismiss(), 300); // Animation delay
}, toast.duration);
return () => clearTimeout(timer);
}
}, [toast]);
const handleDismiss = () => {
setIsVisible(false);
setTimeout(() => toast.dismiss(), 300);
};
return (
<div
className={`toast-item ${toast.type} ${isVisible ? 'visible' : ''}`}
role="alert"
aria-live="polite"
>
<div className="toast-content">
<span className="toast-icon">{getToastIcon(toast.type)}</span>
<span className="toast-message">{toast.message}</span>
{toast.action && (
<button
className="toast-action"
onClick={toast.action.onClick}
>
{toast.action.label}
</button>
)}
{toast.dismissible && (
<button
className="toast-dismiss"
onClick={handleDismiss}
aria-label="Dismiss notification"
>
Γ
</button>
)}
</div>
</div>
);
}
Testing
Toast System Testing
describe('Toast System', () => {
let toastManager: ToastManager;
beforeEach(() => {
toastManager = ToastManager.getInstance();
toastManager.clear();
});
it('should show success toast', () => {
toastManager.success('Test success message');
const toasts = toastManager.getToasts();
expect(toasts).toHaveLength(1);
expect(toasts[0].type).toBe('success');
expect(toasts[0].message).toBe('Test success message');
});
it('should auto-dismiss toast after duration', (done) => {
toastManager.success('Test message', { duration: 100 });
setTimeout(() => {
const toasts = toastManager.getToasts();
expect(toasts).toHaveLength(0);
done();
}, 150);
});
it('should handle toast with action', () => {
const actionSpy = jest.fn();
toastManager.show('Test message', {
action: {
label: 'Test Action',
onClick: actionSpy
}
});
const toasts = toastManager.getToasts();
expect(toasts[0].action?.label).toBe('Test Action');
expect(toasts[0].action?.onClick).toBe(actionSpy);
});
});
This comprehensive toast notification system provides user-friendly feedback with customizable styling, accessibility features, and seamless integration across the PadawanForge application.