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.

PadawanForge v1.4.1