Skip to main content

Presentation Editor Frontend Low-Level Design

Comprehensive technical design for a real-time collaborative presentation editor with rich text editing and slide management capabilities.

This document outlines the low-level architecture and implementation strategy for a feature-rich presentation editor supporting real-time collaboration, rich text editing, and multi-slide management in a modern web application.

1. Problem Statement & Requirements

Clarifying questions

  1. What's the target browser support matrix? (e.g., IE11, mobile Safari, Chrome 90+)
  2. Do we need real-time collaborative editing capabilities or is it primarily static document creation?
  3. Should the editor support offline mode with sync-on-reconnect?
  4. What's the expected content complexity? (text, images, tables, embedded media, etc.)
  5. Is there a design system or component library we should integrate with?
  6. What level of accessibility compliance is required? (WCAG 2.1 AA, AAA?)
  7. Do we need to support internationalization with RTL languages and multiple locales?

Functional requirements

  • Create, edit, and delete slides in a presentation
  • Add various content types to slides (text, images, tables, shapes)
  • Reorder and duplicate slides
  • Apply formatting to text (bold, italic, underline, font size, color)
  • Save and export presentations in various formats (PDF, PPTX, HTML)
  • Undo/redo functionality
  • Slide preview and presentation mode
  • Rich text editing capabilities with WYSIWYG interface

Non-functional requirements

  • Performance:

    • Slide rendering should be under 100ms for content-heavy slides
    • Text editing should maintain <50ms input lag
    • Export operations should complete under 2s for medium-sized presentations
  • Accessibility:

    • Full keyboard navigation support
    • Screen reader compatibility for all interactive elements
    • ARIA roles and semantic HTML structure
  • Scalability:

    • Support for presentations with up to 1000 slides
    • Efficient rendering of large content blocks

Constraints and assumptions

  • Target modern browsers (Chrome 80+, Firefox 75+, Safari 13+)
  • No server-side rendering required for initial load
  • Client-side state management is acceptable for single-user scenarios
  • Assumption that users have stable internet connectivity for save operations

Out of scope

  • Real-time collaborative editing (multi-user simultaneous editing)
  • Advanced animation and transition effects
  • Third-party plugin architecture for content types
  • Cloud storage integration (save to Google Drive, Dropbox)
  • Advanced typography features beyond basic formatting
  • Mobile-first design (desktop-focused experience)

2. Component Architecture

graph TD
    A[PresentationEditor] --> B[Toolbar]
    A --> C[SlideDeck]
    A --> D[SlidePreview]
    A --> E[SlideList]
    A --> F[StatusBar]
    
    C --> G[Slide]
    C --> H[SlideContent]
    G --> I[TextBlock]
    G --> J[ImageBlock]
    G --> K[ShapeBlock]
    
    H --> L[EditableText]
    H --> M[ImageUploader]
    H --> N[VideoPlayer]
    
    D --> O[SlideView]
    O --> P[SlideRenderer]
    
    E --> Q[SlideThumbnail]
    Q --> R[SlideSelector]

Component Tree Breakdown

Smart Components (Container Components):

  • PresentationEditor - Main container managing overall state and routing
  • SlideDeck - Manages slide collection and ordering
  • Toolbar - Handles toolbar actions and state management for active tools
  • SlidePreview - Renders current slide with real-time updates

Dumb Components (Presentational Components):

  • Slide - Immutable slide representation
  • TextBlock, ImageBlock, ShapeBlock - Block-level content elements
  • EditableText - Editable text input with formatting controls
  • SlideThumbnail - Visual representation of slides in slide list
  • StatusBar - Displays metadata and status indicators

TypeScript Props Contracts

// PresentationEditor.tsx
interface PresentationEditorProps {
  initialSlides?: Slide[];
  onSave?: (slides: Slide[]) => Promise<void>;
  onSlideChange?: (slideIndex: number) => void;
}

// Slide.tsx
interface SlideProps {
  index: number;
  slide: Slide;
  isActive?: boolean;
  onSelect?: (index: number) => void;
  onContentChange?: (content: SlideContent) => void;
}

// Toolbar.tsx
interface ToolbarProps {
  activeTool: ToolType;
  onToolChange: (tool: ToolType) => void;
  canUndo: boolean;
  canRedo: boolean;
  onUndo: () => void;
  onRedo: () => void;
  onSave: () => void;
}

// SlidePreview.tsx
interface SlidePreviewProps {
  slide: Slide;
  isEditing?: boolean;
  onContentChange: (content: SlideContent) => void;
}

UI State Machine

stateDiagram-v2
    [*] --> Idle
    Idle --> Editing : Start Edit
    Editing --> Saving : Save
    Saving --> Idle : Success
    Saving --> Error : Failure
    Editing --> Idle : Cancel
    Idle --> Viewing : View Mode
    Viewing --> Editing : Edit Slide
    Error --> Idle : Retry Save

Component Responsibilities

PresentationEditor:

  • Main container for the entire editor experience
  • Manages global state (slides, current slide index, active tool)
  • Handles save/load operations
  • Coordinates between toolbar, preview, and slide list

SlideDeck:

  • Maintains collection of slides in order
  • Handles slide reordering and deletion
  • Manages current active slide index

Toolbar:

  • Provides editing controls (undo/redo, save, tool selection)
  • Manages active editing tools
  • Handles global actions (save, export)

SlidePreview:

  • Renders current slide in view mode
  • Handles real-time content editing
  • Manages slide selection state

SlideList:

  • Displays all slides as thumbnails
  • Handles slide selection and reordering
  • Shows slide titles and metadata

Reusability Decisions

  • TextBlock, ImageBlock, ShapeBlock are designed as generic content elements that can be reused across different slide types
  • EditableText component is designed to be reusable in other content editing contexts
  • Toolbar is kept generic enough to support various editing tools and workflows

Component Lifecycle Considerations

  • PresentationEditor will mount and initialize with default slides or load from storage
  • SlidePreview will receive updates when content changes, triggering re-renders
  • Toolbar will update its enabled/disabled states based on current action availability
  • SlideList will handle drag-and-drop reordering with proper state updates
// SlideContent.tsx - Example of a reusable block component
interface SlideContentProps {
  content: ContentBlock;
  onChange: (newContent: ContentBlock) => void;
  isEditing?: boolean;
}

// SlideRenderer.tsx - Component that renders slide content with proper formatting
interface SlideRendererProps {
  slide: Slide;
  renderMode?: 'preview' | 'export' | 'presentation';
}

3. State Management Design

Local state vs. global state decisions

For a presentation editor, we need to carefully balance local and global state:

  • Local state: Slide-specific content, editor UI states (active slide, selection), temporary undo/redo buffers
  • Global state: User preferences, presentation metadata, shared component configurations, active user session

The decision is driven by the need for granular control over individual slides while maintaining consistent global settings like theme preferences and collaboration state.

State management approach

I'll use Zustand as the primary state management solution for the following reasons:

  1. Simplicity: Minimal boilerplate and easy to understand
  2. TypeScript support: Excellent type inference capabilities
  3. Middleware support: Can easily integrate with devtools and logging
  4. Performance: No unnecessary re-renders from context providers

Zustand is more appropriate than Redux Toolkit for this use case because:

  • The state structure isn't overly complex
  • We don't need the full Redux ecosystem for this feature
  • Zustand's approach to async actions is cleaner than Redux Thunk/Saga

State shape and normalization strategy

// Core state structure
interface PresentationState {
  id: string;
  title: string;
  slides: Slide[];
  metadata: PresentationMetadata;
  theme: ThemeConfig;
  collaborators: Collaborator[];
  activeSlideId: string | null;
  selectedElementIds: string[];
  undoStack: SlideChange[][];
  redoStack: SlideChange[][];
  isSaving: boolean;
  lastSavedAt: Date | null;
}

interface Slide {
  id: string;
  content: ContentBlock[];
  layout: SlideLayout;
  background: BackgroundConfig;
  notes: string;
}

interface ContentBlock {
  id: string;
  type: 'text' | 'image' | 'shape' | 'table';
  content: string | ImageData | ShapeConfig | TableConfig;
  position: Position;
  size: Size;
  styles: BlockStyles;
}

interface SlideChange {
  slideId: string;
  changes: Partial<Slide>;
  timestamp: Date;
}

Server state vs. client state separation

Client state includes:

  • Active slide selection
  • Local editing state (text input, drag positions)
  • UI state (theme, view mode, zoom level)
  • Undo/redo buffers

Server state includes:

  • Presentation metadata (title, author, last modified)
  • Slide content that's persisted to backend
  • Collaborator information and real-time status
  • Version history

Cache invalidation strategy

  1. Slide content cache: Invalidate when slide changes are committed to server
  2. Metadata cache: Invalidate on explicit metadata updates (title, description)
  3. Collaborator cache: Invalidate on user join/leave events
  4. Theme cache: Invalidate when theme preferences change

Optimistic updates design

For collaborative editing, we implement optimistic updates:

  1. When a user makes an edit, update the local state immediately
  2. Send the change to the server asynchronously
  3. If the server rejects the update (conflict), rollback and show error
  4. Merge updates from other users with proper conflict resolution

Critical-path hook implementation

// presentationStore.ts
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';

interface PresentationState {
  id: string;
  title: string;
  slides: Slide[];
  metadata: PresentationMetadata;
  theme: ThemeConfig;
  activeSlideId: string | null;
  selectedElementIds: string[];
  undoStack: SlideChange[][];
  redoStack: SlideChange[][];
  isSaving: boolean;
  lastSavedAt: Date | null;
  // Actions
  setTitle: (title: string) => void;
  addSlide: () => void;
  removeSlide: (slideId: string) => void;
  updateSlideContent: (slideId: string, content: ContentBlock[]) => void;
  setActiveSlide: (slideId: string | null) => void;
  selectElement: (elementId: string) => void;
  deselectElement: (elementId: string) => void;
  undo: () => void;
  redo: () => void;
  savePresentation: () => Promise<void>;
  handleCollaboratorUpdate: (update: CollaboratorUpdate) => void;
}

interface Slide {
  id: string;
  content: ContentBlock[];
  layout: SlideLayout;
  background: BackgroundConfig;
  notes: string;
}

interface ContentBlock {
  id: string;
  type: 'text' | 'image' | 'shape' | 'table';
  content: string | ImageData | ShapeConfig | TableConfig;
  position: Position;
  size: Size;
  styles: BlockStyles;
}

interface SlideChange {
  slideId: string;
  changes: Partial<Slide>;
  timestamp: Date;
}

interface CollaboratorUpdate {
  userId: string;
  action: 'add' | 'remove' | 'update';
  data?: any;
}

const usePresentationStore = create<PresentationState>()(
  devtools(
    persist(
      (set, get) => ({
        // Initial state
        id: '',
        title: 'Untitled Presentation',
        slides: [],
        metadata: {
          author: '',
          createdAt: new Date(),
          lastModified: new Date(),
          version: 1,
        },
        theme: {
          primaryColor: '#3b82f6',
          fontFamily: 'Inter, sans-serif',
          fontSize: '16px',
        },
        activeSlideId: null,
        selectedElementIds: [],
        undoStack: [],
        redoStack: [],
        isSaving: false,
        lastSavedAt: null,

        // Actions
        setTitle: (title) => set({ title }),
        
        addSlide: () => {
          const newSlideId = `slide-${Date.now()}`;
          set((state) => ({
            slides: [
              ...state.slides,
              {
                id: newSlideId,
                content: [],
                layout: { width: 1024, height: 768 },
                background: { type: 'color', value: '#ffffff' },
                notes: '',
              }
            ],
            activeSlideId: newSlideId,
          }));
        },

        removeSlide: (slideId) => {
          set((state) => ({
            slides: state.slides.filter(slide => slide.id !== slideId),
            activeSlideId: state.activeSlideId === slideId ? 
              (state.slides.length > 1 ? state.slides[0].id : null) : 
              state.activeSlideId,
          }));
        },

        updateSlideContent: (slideId, content) => {
          set((state) => ({
            slides: state.slides.map(slide => 
              slide.id === slideId ? { ...slide, content } : slide
            ),
          }));
        },

        setActiveSlide: (slideId) => set({ activeSlideId: slideId }),
        
        selectElement: (elementId) => {
          set((state) => ({
            selectedElementIds: [...state.selectedElementIds, elementId],
          }));
        },

        deselectElement: (elementId) => {
          set((state) => ({
            selectedElementIds: state.selectedElementIds.filter(id => id !== elementId),
          }));
        },

        undo: () => {
          set((state) => {
            if (state.undoStack.length === 0) return state;
            
            const lastChanges = state.undoStack[state.undoStack.length - 1];
            const newUndoStack = state.undoStack.slice(0, -1);
            const newRedoStack = [...state.redoStack, lastChanges];
            
            // Apply reverse changes to state
            const newState = { ...state };
            lastChanges.forEach(change => {
              if (change.slideId) {
                const slideIndex = newState.slides.findIndex(slide => slide.id === change.slideId);
                if (slideIndex !== -1) {
                  newState.slides[slideIndex] = {
                    ...newState.slides[slideIndex],
                    ...change.changes,
                  };
                }
              }
            });
            
            return {
              ...newState,
              undoStack: newUndoStack,
              redoStack: newRedoStack,
            };
          });
        },

        redo: () => {
          set((state) => {
            if (state.redoStack.length === 0) return state;
            
            const nextChanges = state.redoStack[state.redoStack.length - 1];
            const newRedoStack = state.redoStack.slice(0, -1);
            const newUndoStack = [...state.undoStack, nextChanges];
            
            // Apply forward changes to state
            const newState = { ...state };
            nextChanges.forEach(change => {
              if (change.slideId) {
                const slideIndex = newState.slides.findIndex(slide => slide.id === change.slideId);
                if (slideIndex !== -1) {
                  newState.slides[slideIndex] = {
                    ...newState.slides[slideIndex],
                    ...change.changes,
                  };
                }
              }
            });
            
            return {
              ...newState,
              undoStack: newUndoStack,
              redoStack: newRedoStack,
            };
          });
        },

        savePresentation: async () => {
          set({ isSaving: true });
          
          try {
            // Simulate API call
            await new Promise(resolve => setTimeout(resolve, 1000));
            
            set((state) => ({
              lastSavedAt: new Date(),
              isSaving: false,
            }));
          } catch (error) {
            set({ isSaving: false });
            // Handle error with toast or notification
          }
        },

        handleCollaboratorUpdate: (update) => {
          // Handle real-time collaborative updates
          set((state) => ({
            collaborators: update.action === 'add' 
              ? [...state.collaborators, update.data] 
              : state.collaborators.filter(c => c.id !== update.data?.id),
          }));
        },
      }),
      {
        name: 'presentation-storage',
        partialize: (state) => ({
          title: state.title,
          slides: state.slides,
          theme: state.theme,
        }),
      }
    )
  )
);

// Custom hook for presentation context
export const usePresentation = () => {
  const {
    id,
    title,
    slides,
    metadata,
    theme,
    activeSlideId,
    selectedElementIds,
    undoStack,
    redoStack,
    isSaving,
    lastSavedAt,
    setTitle,
    addSlide,
    removeSlide,
    updateSlideContent,
    setActiveSlide,
    selectElement,
    deselectElement,
    undo,
    redo,
    savePresentation,
    handleCollaboratorUpdate,
  } = usePresentationStore();

  return {
    id,
    title,
    slides,
    metadata,
    theme,
    activeSlideId,
    selectedElementIds,
    undoStack,
    redoStack,
    isSaving,
    lastSavedAt,
    setTitle,
    addSlide,
    removeSlide,
    updateSlideContent,
    setActiveSlide,
    selectElement,
    deselectElement,
    undo,
    redo,
    savePresentation,
    handleCollaboratorUpdate,
  };
};

4. Routing & Navigation

Route Structure and Hierarchy

graph TD
    A[Root] --> B[Public Routes]
    A --> C[Protected Routes]
    
    B --> D[/login]
    B --> E[/register]
    B --> F[/forgot-password]
    
    C --> G[/dashboard]
    C --> H[/presentation/:id]
    C --> I[/presentation/:id/edit]
    C --> J[/presentation/:id/view]

The routing structure follows a clear hierarchy with public and protected routes. Protected routes require authentication and are organized under a /dashboard layout.

Code Splitting Strategy

Route-based code splitting with dynamic imports:

// routes.tsx
const DashboardLayout = React.lazy(() => import('layouts/DashboardLayout'));
const PresentationEditor = React.lazy(() => import('features/presentation-editor/PresentationEditor'));
const PresentationViewer = React.lazy(() => import('features/presentation-viewer/PresentationViewer'));

// Route configuration with lazy loading
const routes = [
  {
    path: '/dashboard',
    element: <DashboardLayout />,
    children: [
      {
        path: 'presentation/:id/edit',
        element: <PresentationEditor />,
        loader: presentationLoader,
      },
      {
        path: 'presentation/:id/view',
        element: <PresentationViewer />,
        loader: presentationLoader,
      },
    ],
  },
];

Justification:

  • /presentation/:id/edit and /presentation/:id/view are large bundles with complex editor components
  • DashboardLayout is shared across multiple protected routes and should be loaded once
  • Route-level splitting reduces initial bundle size and improves load times
// auth-guard.tsx
const AuthGuard: React.FC<{ children: React.ReactNode }> = ({ children }) => {
  const { isAuthenticated, isLoading } = useAuth();
  
  if (isLoading) return <LoadingSpinner />;
  
  if (!isAuthenticated) {
    return <Navigate to="/login" replace />;
  }
  
  return children;
};

// Protected route wrapper
const ProtectedRoute: React.FC<{ 
  element: React.ReactNode; 
  requiresAuth?: boolean;
}> = ({ element, requiresAuth = true }) => {
  if (requiresAuth) {
    return <AuthGuard>{element}</AuthGuard>;
  }
  
  return element;
};

Deep Linking Support and URL Parameter Design

// URL structure for deep linking:
// /presentation/12345/edit?slide=2&mode=edit
// /presentation/12345/view?slide=2&mode=view

// Route params and query parsing
interface PresentationRouteParams {
  id: string;
}

interface PresentationRouteQuery {
  slide?: string;
  mode?: 'edit' | 'view';
  theme?: string;
}

// URL parameter validation
const validatePresentationId = (id: string): boolean => {
  return /^[0-9a-f]{24}$/.test(id); // MongoDB ObjectId format
};

Browser History Management and Scroll Restoration

// Custom scroll restoration hook
const useScrollRestoration = () => {
  useEffect(() => {
    const handleScroll = () => {
      // Store scroll position in session storage
      sessionStorage.setItem('scrollPosition', window.scrollY.toString());
    };

    window.addEventListener('scroll', handleScroll);
    
    // Restore scroll position on navigation
    const savedPosition = sessionStorage.getItem('scrollPosition');
    if (savedPosition) {
      window.scrollTo(0, parseInt(savedPosition));
    }

    return () => {
      window.removeEventListener('scroll', handleScroll);
    };
  }, []);
};

// Enhanced router with scroll restoration
const RouterWithScrollRestoration = () => {
  const location = useLocation();
  
  useEffect(() => {
    // Reset scroll to top on route change
    window.scrollTo(0, 0);
  }, [location.pathname]);

  return (
    <BrowserRouter>
      <ScrollRestoration />
      <Routes>
        {/* Route definitions */}
      </Routes>
    </BrowserRouter>
  );
};
// Navigation utility functions
const usePresentationNavigation = (presentationId: string) => {
  const navigate = useNavigate();
  
  const goToEdit = (slideId?: string) => {
    const params = new URLSearchParams();
    if (slideId) params.set('slide', slideId);
    
    navigate(`/presentation/${presentationId}/edit?${params.toString()}`);
  };
  
  const goToView = (slideId?: string) => {
    const params = new URLSearchParams();
    if (slideId) params.set('slide', slideId);
    
    navigate(`/presentation/${presentationId}/view?${params.toString()}`);
  };
  
  const goToDashboard = () => {
    navigate('/dashboard');
  };
  
  return { goToEdit, goToView, goToDashboard };
};

// Navigation context for consistent navigation across components
const NavigationContext = createContext<{
  navigateToEdit: (id: string, slideId?: string) => void;
  navigateToView: (id: string, slideId?: string) => void;
  navigateToDashboard: () => void;
}>({
  navigateToEdit: () => {},
  navigateToView: () => {},
  navigateToDashboard: () => {},
});

const NavigationProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
  const navigate = useNavigate();
  
  const navigateToEdit = (id: string, slideId?: string) => {
    const params = new URLSearchParams();
    if (slideId) params.set('slide', slideId);
    navigate(`/presentation/${id}/edit?${params.toString()}`);
  };
  
  const navigateToView = (id: string, slideId?: string) => {
    const params = new URLSearchParams();
    if (slideId) params.set('slide', slideId);
    navigate(`/presentation/${id}/view?${params.toString()}`);
  };
  
  const navigateToDashboard = () => {
    navigate('/dashboard');
  };
  
  return (
    <NavigationContext.Provider value={{
      navigateToEdit,
      navigateToView,
      navigateToDashboard
    }}>
      {children}
    </NavigationContext.Provider>
  );
};

5. API Integration Layer

API Client Design Pattern

We'll implement a centralized API client using a fetch wrapper with Axios as the underlying HTTP client. This approach provides better request/response interception, error handling, and type safety than raw fetch.

// api/client.ts
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';

const createApiClient = (): AxiosInstance => {
  const client = axios.create({
    baseURL: process.env.NEXT_PUBLIC_API_BASE_URL || 'https://api.example.com',
    timeout: 10000,
    headers: {
      'Content-Type': 'application/json',
      'Accept': 'application/json',
    },
  });

  // Request interceptor for auth token injection
  client.interceptors.request.use(
    (config) => {
      const token = localStorage.getItem('authToken');
      if (token) {
        config.headers.Authorization = `Bearer ${token}`;
      }
      return config;
    },
    (error) => Promise.reject(error)
  );

  // Response interceptor for error handling and normalization
  client.interceptors.response.use(
    (response) => response,
    (error) => {
      if (error.response?.status === 401) {
        // Handle auth errors
        localStorage.removeItem('authToken');
        window.location.href = '/login';
      }
      return Promise.reject(error);
    }
  );

  return client;
};

export const apiClient = createApiClient();

Request/Response Interceptors

The request interceptor handles authentication token injection and includes a retry mechanism for transient network errors. The response interceptor normalizes error responses and handles authentication failures.

Error Handling and Retry Strategy

We implement a retry strategy with exponential backoff for transient errors (429, 502, 503). For client errors (4xx), we don't retry as they typically indicate permanent issues.

// api/retry.ts
export const retryWithBackoff = async (
  fn: () => Promise<any>,
  retries = 3,
  delay = 1000
): Promise<any> => {
  for (let i = 0; i <= retries; i++) {
    try {
      return await fn();
    } catch (error) {
      if (i === retries) throw error;
      
      const shouldRetry = [429, 502, 503].includes(error.response?.status);
      if (!shouldRetry) throw error;
      
      await new Promise(resolve => setTimeout(resolve, delay * Math.pow(2, i)));
    }
  }
};

Loading, Error, and Empty States Design

We implement a standardized loading state pattern using React Query's built-in loading states and custom error boundaries. Each API operation returns a consistent structure:

// types/api.ts
export interface ApiResponse<T> {
  data: T | null;
  loading: boolean;
  error: Error | null;
  refetch: () => void;
}

export interface Presentation {
  id: string;
  title: string;
  slides: Slide[];
  createdAt: Date;
  updatedAt: Date;
}

export interface Slide {
  id: string;
  content: string;
  layout: 'title' | 'content' | 'blank';
  position: number;
}

Data Fetching Strategy

For a presentation editor, we use CSR (Client-Side Rendering) with React Query for data fetching. This allows real-time editing and immediate feedback:

  • Presentation list: CSR with caching
  • Single presentation: CSR with optimistic updates for edits
  • Media uploads: Direct upload to cloud storage (e.g., AWS S3) with presigned URLs

API Response Transformation Layer

We transform raw API responses into domain models to decouple frontend from backend structure changes:

// api/transformers.ts
import { Presentation, Slide } from '../types/domain';
import { ApiPresentation, ApiSlide } from '../types/api';

export const transformApiToDomain = (apiPresentation: ApiPresentation): Presentation => ({
  id: apiPresentation.id,
  title: apiPresentation.title,
  slides: apiPresentation.slides.map(transformSlide),
  createdAt: new Date(apiPresentation.created_at),
  updatedAt: new Date(apiPresentation.updated_at),
});

const transformSlide = (apiSlide: ApiSlide): Slide => ({
  id: apiSlide.id,
  content: apiSlide.content,
  layout: apiSlide.layout,
  position: apiSlide.position,
});

Sequence Diagram for Primary Data Flows

sequenceDiagram
    participant U as User
    participant FE as Frontend
    participant API as Backend API
    participant DB as Database

    U->>FE: Load presentation
    FE->>API: GET /presentations/{id}
    API->>DB: Query presentation
    DB-->>API: Return presentation data
    API-->>FE: Send JSON response
    FE->>FE: Transform and cache data
    FE-->>U: Render presentation

    U->>FE: Update slide content
    FE->>API: PUT /presentations/{id}/slides/{slideId}
    API->>DB: Update slide in DB
    DB-->>API: Confirm update
    API-->>FE: Return updated slide
    FE->>FE: Update local state and cache
    FE-->>U: Show updated content

    U->>FE: Add new slide
    FE->>API: POST /presentations/{id}/slides
    API->>DB: Insert new slide
    DB-->>API: Return new slide ID
    API-->>FE: Send updated presentation
    FE->>FE: Update local state and cache
    FE-->>U: Show new slide in UI

Media Upload Integration

For media uploads, we implement a two-step process:

  1. Request presigned URL from backend
  2. Upload directly to cloud storage (S3-compatible)
  3. Update presentation with media reference
// api/media.ts
export const uploadMedia = async (file: File): Promise<string> => {
  // Step 1: Get presigned URL
  const response = await apiClient.post('/media/upload', {
    filename: file.name,
    contentType: file.type,
  });
  
  const { url, fields } = response.data;
  
  // Step 2: Upload to S3
  const formData = new FormData();
  Object.entries(fields).forEach(([key, value]) => {
    formData.append(key, value as string);
  });
  formData.append('file', file);
  
  await fetch(url, {
    method: 'POST',
    body: formData,
  });
  
  // Step 3: Return media URL
  return `${url.split('?')[0]}`;
};

6. Performance Design

Core Web Vitals Targets

  • Largest Contentful Paint (LCP): <2.5s for all slide content
  • Interaction to Next Paint (INP): <200ms for user interactions
  • Cumulative Layout Shift (CLS): <0.1 for all slide content and UI elements
  • First Contentful Paint (FCP): <1.8s for initial slide load

Bundle Size Budget per Route

  • Presentation Editor Route: <500KB (including all dependencies, excluding assets)
  • Slide Preview Mode: <300KB
  • Export/Download Route: <200KB

Critical Rendering Path Optimization

  • Critical CSS: Inline essential styles for slide rendering (20KB max)
  • Async Script Loading: Non-critical scripts (e.g., analytics, third-party embeds) loaded with defer or async
  • Resource Hints: Preload key assets like font files and slide templates
  • Code Splitting: Route-based chunking with dynamic imports for heavy features

Image Optimization Strategy

  • Format Selection: WebP for modern browsers, JPEG fallback for legacy
  • Lazy Loading: loading="lazy" attribute on all slide images
  • Responsive Images: srcset with multiple sizes for different viewports
  • Image Compression: 85% quality JPEG, 90% WebP with progressive loading

Font Loading Strategy

  • Font Display: swap for all presentation fonts to prevent FOIT
  • Preload Critical Fonts: preload for primary font families in HTML head
  • Font Subsets: Load only necessary character sets (Latin, Cyrillic, etc.)
  • Fallback Fonts: Define appropriate fallbacks for all font stacks

Memoization Strategy

// Memoize expensive slide rendering operations
const SlideRenderer = React.memo(({ slide, theme }: SlideRendererProps) => {
  const renderedContent = useMemo(() => {
    return renderSlideContent(slide, theme);
  }, [slide.content, theme]);

  return <div dangerouslySetInnerHTML={{ __html: renderedContent }} />;
});

// Memoize event handlers that depend on props
const useSlideEventHandler = (slideId: string, onEdit: () => void) => {
  return useCallback((e: React.MouseEvent) => {
    if (e.target instanceof HTMLElement && e.target.closest('.editable')) {
      onEdit();
    }
  }, [slideId, onEdit]);
};

Virtualization for Large Lists

// For slide decks with >50 slides, use react-window
const SlideList = ({ slides, onSlideSelect }: SlideListProps) => {
  const slideCount = slides.length;
  
  return (
    <FixedSizeList
      height={500}
      itemCount={slideCount}
      itemSize={80}
      width="100%"
    >
      {({ index, style }) => (
        <div style={style}>
          <SlidePreview 
            slide={slides[index]} 
            isSelected={selectedIndex === index}
            onClick={() => onSlideSelect(index)}
          />
        </div>
      )}
    </FixedSizeList>
  );
};

Prefetching and Preloading Strategy

  • Template Prefetch: Preload slide templates when user navigates to editor
  • Asset Preload: When user creates a new presentation, prefetch common assets (icons, default themes)
  • Slide Preload: When user navigates to a presentation with >5 slides, preload first 3 slides
  • Export Preload: When user opens export modal, prefetch all slide data and generate preview thumbnails
// Prefetching strategy hook
const usePrefetchStrategy = (presentationId: string) => {
  useEffect(() => {
    // Prefetch templates and themes
    const prefetchTemplates = fetch('/api/templates');
    const prefetchThemes = fetch('/api/themes');
    
    // Prefetch first 3 slides
    const prefetchSlides = fetch(`/api/presentations/${presentationId}/slides?limit=3`);
    
    Promise.all([prefetchTemplates, prefetchThemes, prefetchSlides])
      .catch(err => console.error('Prefetch failed:', err));
  }, [presentationId]);
};

7. Error Handling & Resilience

Error Boundary Placement Strategy

Error boundaries should be strategically placed to prevent cascading failures while maintaining user experience. The component tree structure for error boundaries:

graph TD
    A[App] --> B[ErrorBoundary]
    B --> C[DashboardLayout]
    B --> D[EditorLayout]
    B --> E[PreviewLayout]
    C --> F[SlideList]
    C --> G[SlideEditor]
    D --> H[SlidePreview]
    D --> I[SlideToolbar]
    E --> J[SlideExport]

Key placement decisions:

  • Top-level App boundary for unhandled errors
  • Layout boundaries to contain component-specific failures
  • Slide editor and preview components with their own error boundaries

Global Error Handler Design

// src/core/error/GlobalErrorHandler.tsx
import React from 'react';

interface GlobalErrorState {
  hasError: boolean;
  error?: Error;
  errorInfo?: React.ErrorInfo;
}

class GlobalErrorHandler extends React.Component<{}, GlobalErrorState> {
  constructor(props: {}) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error: Error): GlobalErrorState {
    return { hasError: true, error };
  }

  componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
    // Log to monitoring service
    console.error('Global error caught:', error, errorInfo);
    
    // Report to external error tracking (Sentry)
    if (window.Sentry) {
      window.Sentry.captureException(error, {
        contexts: { react: errorInfo }
      });
    }
  }

  render() {
    if (this.state.hasError) {
      return (
        <div className="error-container">
          <h1>Something went wrong.</h1>
          <p>We're working to fix this issue.</p>
          <button onClick={() => window.location.reload()}>
            Reload Page
          </button>
        </div>
      );
    }

    return this.props.children;
  }
}

export default GlobalErrorHandler;

Offline / Network Failure Handling

// src/core/network/NetworkStatus.ts
import { useState, useEffect } from 'react';

export const useNetworkStatus = () => {
  const [isOnline, setIsOnline] = useState<boolean>(true);
  const [connectionQuality, setConnectionQuality] = useState<'excellent' | 'good' | 'fair' | 'poor'>('excellent');

  useEffect(() => {
    const handleOnline = () => setIsOnline(true);
    const handleOffline = () => setIsOnline(false);

    window.addEventListener('online', handleOnline);
    window.addEventListener('offline', handleOffline);

    return () => {
      window.removeEventListener('online', handleOnline);
      window.removeEventListener('offline', handleOffline);
    };
  }, []);

  // Simulate connection quality based on network metrics
  useEffect(() => {
    const checkConnectionQuality = () => {
      if (!isOnline) {
        setConnectionQuality('poor');
        return;
      }

      // Mock quality detection logic
      const rtt = Math.random() * 200; // ms
      if (rtt < 50) {
        setConnectionQuality('excellent');
      } else if (rtt < 150) {
        setConnectionQuality('good');
      } else if (rtt < 300) {
        setConnectionQuality('fair');
      } else {
        setConnectionQuality('poor');
      }
    };

    const interval = setInterval(checkConnectionQuality, 5000);
    return () => clearInterval(interval);
  }, [isOnline]);

  return { isOnline, connectionQuality };
};

Graceful Degradation Patterns

// src/features/editor/SlideEditor.tsx
const SlideEditor = ({ slideId }: { slideId: string }) => {
  const [editorState, setEditorState] = useState<SlideState | null>(null);
  const [isOffline, setIsOffline] = useState(false);
  const [error, setError] = useState<Error | null>(null);

  useEffect(() => {
    const handleOffline = () => setIsOffline(true);
    const handleOnline = () => setIsOffline(false);

    window.addEventListener('offline', handleOffline);
    window.addEventListener('online', handleOnline);

    return () => {
      window.removeEventListener('offline', handleOffline);
      window.removeEventListener('online', handleOnline);
    };
  }, []);

  // Fallback UI for offline mode
  if (isOffline && !editorState) {
    return (
      <div className="offline-fallback">
        <p>You're currently offline. Editing is disabled.</p>
        <p>Changes will be saved when you reconnect.</p>
      </div>
    );
  }

  // Fallback for corrupted slide data
  if (error) {
    return (
      <div className="error-fallback">
        <p>Failed to load slide content.</p>
        <button onClick={() => window.location.reload()}>
          Retry
        </button>
      </div>
    );
  }

  return (
    <div className="slide-editor">
      {/* Editor content */}
    </div>
  );
};

User Feedback on Errors

// src/core/toast/ToastManager.tsx
import React from 'react';
import { ToastContainer, toast } from 'react-toastify';

interface ToastOptions {
  type?: 'success' | 'error' | 'info' | 'warning';
  autoClose?: number;
  position?: 'top-right' | 'top-center' | 'top-left' | 'bottom-right' | 'bottom-center' | 'bottom-left';
}

export const ToastManager = () => (
  <ToastContainer
    position="top-right"
    autoClose={5000}
    hideProgressBar={false}
    newestOnTop
    closeOnClick
    rtl={false}
    pauseOnFocusLoss
    draggable
    pauseOnHover
  />
);

export const showErrorMessage = (message: string, options?: ToastOptions) => {
  toast.error(message, {
    type: 'error',
    autoClose: 3000,
    ...options
  });
};

export const showWarningMessage = (message: string, options?: ToastOptions) => {
  toast.warning(message, {
    type: 'warning',
    autoClose: 5000,
    ...options
  });
};

export const showSuccessMessage = (message: string, options?: ToastOptions) => {
  toast.success(message, {
    type: 'success',
    autoClose: 2000,
    ...options
  });
};

Error Recovery Strategies

// src/features/editor/hooks/useEditorRecovery.ts
import { useState, useEffect } from 'react';
import { useDebounce } from 'use-debounce';

export const useEditorRecovery = (slideId: string) => {
  const [recoveryState, setRecoveryState] = useState({
    hasUnsavedChanges: false,
    lastSaved: null as Date | null,
    recoveryPoints: [] as RecoveryPoint[]
  });

  const [debouncedSlideContent] = useDebounce(slideId, 2000);

  useEffect(() => {
    // Auto-save to localStorage when content changes
    const saveRecoveryPoint = () => {
      if (debouncedSlideContent) {
        try {
          const recoveryPoint: RecoveryPoint = {
            timestamp: new Date(),
            content: debouncedSlideContent,
            slideId
          };
          
          // Store in localStorage with timestamp
          const recoveryKey = `recovery_${slideId}`;
          localStorage.setItem(recoveryKey, JSON.stringify(recoveryPoint));
          
          setRecoveryState(prev => ({
            ...prev,
            hasUnsavedChanges: true,
            lastSaved: new Date()
          }));
        } catch (err) {
          console.error('Failed to save recovery point:', err);
          showErrorMessage('Failed to save local changes');
        }
      }
    };

    // Debounced save
    const timeoutId = setTimeout(saveRecoveryPoint, 1000);
    
    return () => clearTimeout(timeoutId);
  }, [debouncedSlideContent, slideId]);

  const recoverFromLocal = () => {
    try {
      const recoveryKey = `recovery_${slideId}`;
      const recoveryPointStr = localStorage.getItem(recoveryKey);
      
      if (recoveryPointStr) {
        const recoveryPoint = JSON.parse(recoveryPointStr);
        showSuccessMessage('Recovered unsaved changes');
        return recoveryPoint.content;
      }
    } catch (err) {
      console.error('Failed to recover from local storage:', err);
      showErrorMessage('Failed to recover local changes');
    }
    
    return null;
  };

  const clearRecoveryPoint = () => {
    try {
      const recoveryKey = `recovery_${slideId}`;
      localStorage.removeItem(recoveryKey);
      setRecoveryState(prev => ({
        ...prev,
        hasUnsavedChanges: false,
        lastSaved: new Date()
      }));
    } catch (err) {
      console.error('Failed to clear recovery point:', err);
    }
  };

  return {
    ...recoveryState,
    recoverFromLocal,
    clearRecoveryPoint
  };
};

interface RecoveryPoint {
  timestamp: Date;
  content: string;
  slideId: string;
}

8. Testing Strategy

Unit Testing Approach

Component Tests (70% coverage target):

  • Core presentation components: Slide, SlideDeck, Toolbar, Canvas
  • Custom hooks: useEditorState, useSlideSelection, useUndoRedo
  • Utility functions: slide manipulation, serialization, validation

Utility Tests (80% coverage target):

  • Data transformation: JSON ↔ slide format conversion
  • Validation logic: slide content, layout constraints
  • Formatting helpers: text styling, image sizing

Test Structure:

// Example component test structure
describe('SlideComponent', () => {
  it('renders slide content correctly', () => {
    const { getByText } = render(<Slide content="Hello World" />);
    expect(getByText('Hello World')).toBeInTheDocument();
  });

  it('handles selection state properly', () => {
    const { container } = render(<Slide selected={true} />);
    expect(container.firstChild).toHaveClass('selected');
  });
});

Integration Testing Strategy

Feature Integration Tests (50% coverage target):

  • Full editor workflow: create → edit → save → export
  • Multi-slide interactions: drag/drop, copy/paste, rearrange
  • Toolbar functionality: formatting, undo/redo, export

Cross-component Tests:

  • Slide selection sync between canvas and sidebar
  • State persistence across component re-renders
  • Undo/redo chain consistency

E2E Testing Scope

Cypress Test Coverage:

  • Core user flows: create new presentation, add slides, edit content
  • Complex interactions: drag-and-drop slide reordering, multi-select operations
  • Export functionality: PDF, PNG, JSON format generation
  • Multi-user collaboration scenarios (if applicable)

Critical User Journeys:

  1. Create new presentation from template
  2. Add multiple slides with different content types
  3. Format text and apply styles
  4. Export presentation to various formats
  5. Save and reload presentation state

Visual Regression Testing

Screenshot Strategy:

  • Test key presentation states: empty deck, single slide, multi-slide
  • Validate different content types: text, images, shapes, tables
  • Test responsive layouts across device sizes

Tool: Percy or Chromatic with baseline snapshots for each release.

Accessibility Testing Automation

Axe-core Integration:

// Example accessibility test
it('maintains WCAG 2.1 AA compliance', async () => {
  const { container } = render(<PresentationEditor />);
  const results = await axe.run(container);
  expect(results.violations).toHaveLength(0);
});

Testing Coverage:

  • Keyboard navigation through all toolbar items
  • Screen reader compatibility for content elements
  • Focus order consistency across interactive components

Test Data Strategy

Test Fixtures:

// Sample test data structure
const mockSlides = [
  {
    id: 'slide-1',
    content: '<p>Hello World</p>',
    layout: 'title-content',
    metadata: { createdAt: new Date() }
  },
  {
    id: 'slide-2',
    content: '<img src="test.png" alt="Test image" />',
    layout: 'image',
    metadata: { createdAt: new Date() }
  }
];

const mockPresentation = {
  id: 'presentation-1',
  title: 'Test Presentation',
  slides: mockSlides,
  metadata: { lastModified: new Date() }
};

Data Generation:

  • Use Faker.js for realistic content generation
  • Mock API responses with different error conditions
  • Test edge cases: empty slides, malformed content, large datasets

Test Environment:

  • Isolated test databases for stateful operations
  • Mocked external services (image upload, export APIs)
  • Consistent test data across CI/CD pipeline runs

9. Accessibility (A11y)

WCAG 2.1 AA compliance checklist for this feature

The presentation editor must meet WCAG 2.1 AA standards across all interactive elements:

  • Text Contrast: All text must have a contrast ratio of at least 4.5:1 against its background
  • Keyboard Navigation: Full keyboard accessibility for all interactive elements (Tab, Shift+Tab, Enter, Space)
  • Focus Management: Proper focus order and visible focus indicators for all interactive elements
  • Screen Reader Support: ARIA labels and roles for all custom UI components
  • Input Validation: Clear error messages with proper ARIA attributes for form validation
  • Alternative Text: Image alt text support for media elements in slides

Keyboard navigation support

All editor functionality must be fully operable via keyboard:

// Keyboard navigation handler for slide navigation
const handleKeyboardNavigation = (e: KeyboardEvent) => {
  if (e.key === 'ArrowRight') {
    handleNextSlide();
  } else if (e.key === 'ArrowLeft') {
    handlePrevSlide();
  } else if (e.key === 'Enter' && e.target instanceof HTMLElement) {
    // Enter key to edit content
    if (e.target.classList.contains('editable-content')) {
      activateEditor(e.target);
    }
  }
};

Focus management strategy

  • Focus Trapping: When modals open (e.g., media upload), focus is trapped within the modal
  • Focus Restoration: When modals close, focus returns to the previously focused element
  • Logical Tab Order: Elements follow a logical reading order that matches content hierarchy

ARIA roles and attributes plan

// Slide component with proper ARIA attributes
const SlideComponent = ({ 
  slideId, 
  title, 
  content, 
  isActive,
  onEdit
}: {
  slideId: string;
  title: string;
  content: string;
  isActive: boolean;
  onEdit: () => void;
}) => {
  return (
    <div 
      role="article"
      aria-labelledby={`slide-title-${slideId}`}
      aria-describedby={`slide-content-${slideId}`}
      tabIndex={isActive ? 0 : -1}
      className={`slide ${isActive ? 'active' : ''}`}
    >
      <h2 
        id={`slide-title-${slideId}`} 
        className="slide-title"
        tabIndex={0}
        onClick={onEdit}
      >
        {title}
      </h2>
      <div 
        id={`slide-content-${slideId}`}
        className="slide-content"
        contentEditable
        role="textbox"
        aria-label={`Slide content for ${title}`}
        onInput={handleContentChange}
      >
        {content}
      </div>
    </div>
  );
};

Screen reader compatibility

  • Semantic HTML: Use proper heading hierarchy (h1, h2, etc.) for content structure
  • Live Regions: Use aria-live for real-time updates (e.g., slide count changes)
  • Dynamic Content Updates: Properly announce content changes without disrupting screen reader flow
  • Alternative Navigation: Keyboard shortcuts and ARIA landmarks for quick navigation between slides
// Screen reader announcement utility
const announceToScreenReader = (message: string) => {
  const liveRegion = document.getElementById('sr-announcement');
  if (liveRegion) {
    liveRegion.textContent = message;
    setTimeout(() => {
      liveRegion.textContent = '';
    }, 1000);
  }
};

Additional A11y considerations

  • High Contrast Mode Support: CSS variables for theme customization that work with high contrast settings
  • Zoom Compatibility: Content must remain readable at 200% zoom level
  • Color Blindness Consideration: Color palette that works for colorblind users (avoid red/green combinations)
  • Time-based Content: No time limits that would prevent users from completing tasks
  • Error Handling: Clear error messages with proper ARIA attributes for accessibility

Focus trapping implementation

// Focus trap hook for modals
const useFocusTrap = (isOpen: boolean, modalRef: React.RefObject<HTMLElement>) => {
  useEffect(() => {
    if (!isOpen) return;

    const handleFocusTrap = (e: KeyboardEvent) => {
      if (e.key === 'Tab') {
        const focusableElements = modalRef.current?.querySelectorAll(
          'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
        ) || [];
        
        const firstElement = focusableElements[0] as HTMLElement;
        const lastElement = focusableElements[focusableElements.length - 1] as HTMLElement;

        if (e.shiftKey && document.activeElement === firstElement) {
          lastElement.focus();
          e.preventDefault();
        } else if (!e.shiftKey && document.activeElement === lastElement) {
          firstElement.focus();
          e.preventDefault();
        }
      }
    };

    document.addEventListener('keydown', handleFocusTrap);
    return () => document.removeEventListener('keydown', handleFocusTrap);
  }, [isOpen]);
};

10. Security Considerations

XSS Prevention Strategy

Content Sanitization: All user-generated content must be sanitized before rendering to prevent XSS attacks. Implement a robust HTML sanitizer that strips dangerous tags and attributes while preserving safe formatting:

// Example using DOMPurify for content sanitization
import DOMPurify from 'dompurify';

const sanitizeContent = (content: string): string => {
  return DOMPurify.sanitize(content, {
    ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'u', 'ol', 'ul', 'li'],
    ALLOWED_ATTR: ['class'],
    ALLOWED_URI_REGEXP: /^https?:\/\/[^\s]+$/,
  });
};

Input Validation: All content inputs must be validated server-side and client-side with strict sanitization rules. Reject any content containing:

  • Script tags (<script>)
  • Event handlers (onload, onclick, etc.)
  • JavaScript URLs (javascript:)
  • CSS expressions (expression())

CSRF Protection

Token Management: Implement CSRF tokens for all state-changing operations:

// Token generation and validation in API client
const generateCSRFToken = () => {
  const token = crypto.randomUUID();
  localStorage.setItem('csrf_token', token);
  return token;
};

// API client interceptor
const apiClient = axios.create({
  baseURL: '/api',
  withCredentials: true,
});

apiClient.interceptors.request.use((config) => {
  const token = localStorage.getItem('csrf_token');
  if (token) {
    config.headers['X-CSRF-Token'] = token;
  }
  return config;
});

Content Security Policy (CSP) Headers

Header Configuration: Implement strict CSP headers to prevent script execution from unauthorized sources:

Content-Security-Policy: 
  default-src 'self';
  script-src 'self' 'unsafe-inline' 'unsafe-eval';
  style-src 'self' 'unsafe-inline';
  img-src 'self' data: https:;
  font-src 'self' data:;
  connect-src 'self';
  frame-ancestors 'none';

Sensitive Data Handling

State Security: Never store sensitive data in frontend state or localStorage:

// Secure state management pattern
interface PresentationState {
  // Never store sensitive content in frontend state
  title: string;
  slides: Slide[];
  metadata: {
    id: string;
    createdAt: Date;
    updatedAt: Date;
  };
}

// Sensitive content is handled via secure API calls
const handleContentChange = (slideId: string, content: string) => {
  // Validate and sanitize content before API call
  const sanitizedContent = sanitizeContent(content);
  
  // Send to backend for storage and processing
  return apiClient.put(`/slides/${slideId}/content`, {
    content: sanitizedContent,
  });
};

Secure Storage: Use secure HTTP-only cookies for authentication tokens instead of localStorage:

// Secure token handling
const setSecureToken = (token: string) => {
  document.cookie = `auth_token=${token}; 
                     Secure; 
                     HttpOnly; 
                     SameSite=Strict; 
                     Max-Age=${30 * 24 * 60 * 60}`;
};

Input Sanitization Approach

Comprehensive Validation: Implement multi-layered input validation:

  1. Client-side validation with strict regex patterns
  2. Server-side validation with comprehensive sanitization
  3. Content type restrictions for file uploads (only allow presentation formats)
  4. Size limits to prevent memory exhaustion attacks
// Input validation utility
const validateInput = (input: string): ValidationResult => {
  // Check for dangerous patterns
  const dangerousPatterns = [
    /<script[^>]*>.*?<\/script>/i,
    /javascript:/i,
    /on\w+\s*=/i,
  ];

  const hasDangerousContent = dangerousPatterns.some(pattern => 
    pattern.test(input)
  );

  if (hasDangerousContent) {
    return { isValid: false, error: 'Content contains unsafe elements' };
  }

  // Check length limits
  if (input.length > MAX_CONTENT_LENGTH) {
    return { isValid: false, error: 'Content exceeds maximum length' };
  }

  return { isValid: true };
};

File Upload Security: Implement strict file type validation and virus scanning for uploaded assets:

// File upload validation
const validateFileUpload = (file: File): Promise<boolean> => {
  const allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
  const maxFileSize = 5 * 1024 * 1024; // 5MB

  if (!allowedTypes.includes(file.type)) {
    throw new Error('Invalid file type');
  }

  if (file.size > maxFileSize) {
    throw new Error('File too large');
  }

  // Server-side virus scanning would be implemented here
  return Promise.resolve(true);
};

Security Headers Implementation

Header Configuration: Implement security headers in server configuration:

// Server-side security headers (example Node.js/Express)
app.use((req, res, next) => {
  res.setHeader('X-Content-Type-Options', 'nosniff');
  res.setHeader('X-Frame-Options', 'DENY');
  res.setHeader('X-XSS-Protection', '1; mode=block');
  res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
  next();
});

Defense in Depth

Multiple Validation Layers: Implement validation at multiple levels:

  1. Frontend input sanitization
  2. API request validation
  3. Database content sanitization
  4. Content rendering sanitization

Rate Limiting: Implement rate limiting for API endpoints to prevent abuse:

// Rate limiting middleware (example)
const rateLimit = require('express-rate-limit');

const presentationLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100, // limit each IP to 100 requests per windowMs
  message: 'Too many presentation requests from this IP'
});

Audit and Monitoring

Security Logging: Implement security event logging for suspicious activities:

// Security audit logging
const logSecurityEvent = (event: string, details: any) => {
  if (process.env.NODE_ENV === 'production') {
    // Log to centralized security monitoring system
    console.warn(`[SECURITY] ${event}:`, details);
  }
};

Vulnerability Scanning: Regular security scanning of dependencies and frontend code using tools like:

  • Snyk for dependency vulnerabilities
  • OWASP ZAP for web application scanning
  • CodeQL for static analysis

Risk Mitigation

Zero Trust Architecture: Assume all content is potentially malicious and validate everything:

  1. Never trust user input
  2. Sanitize all content before processing
  3. Implement proper access controls
  4. Regular security audits and penetration testing

Secure Development Practices:

  • Implement security reviews in code review process
  • Use security-focused linters and tools
  • Regular security training for development team
  • Maintain up-to-date security dependencies

11. Internationalization (i18n)

Translation Key Structure and Naming Convention

Following BEM-style naming with feature-scoped keys to ensure maintainability:

// Translation keys structure
{
  "presentation": {
    "editor": {
      "title": "Presentation Editor",
      "addSlide": "Add Slide",
      "deleteSlide": "Delete Slide",
      "duplicateSlide": "Duplicate Slide",
      "moveUp": "Move Up",
      "moveDown": "Move Down"
    },
    "slide": {
      "titlePlaceholder": "Slide Title",
      "contentPlaceholder": "Add content here...",
      "emptyState": "No slides yet. Add your first slide."
    },
    "toolbar": {
      "bold": "Bold",
      "italic": "Italic",
      "underline": "Underline",
      "fontSize": "Font Size"
    }
  },
  "common": {
    "save": "Save",
    "cancel": "Cancel",
    "confirm": "Confirm",
    "loading": "Loading..."
  }
}

RTL Support Considerations

RTL layout handling requires careful attention to:

  • Component direction properties (dir="rtl")
  • Text alignment (left/right)
  • Button order in toolbars
  • Slide navigation arrow positioning

Date, Number, Currency Formatting Strategy

Using Intl API for locale-aware formatting:

const formatDate = (date: Date, locale: string) => {
  return new Intl.DateTimeFormat(locale, {
    year: 'numeric',
    month: 'long',
    day: 'numeric'
  }).format(date);
};

const formatNumber = (value: number, locale: string) => {
  return new Intl.NumberFormat(locale, {
    minimumFractionDigits: 2,
    maximumFractionDigits: 2
  }).format(value);
};

Locale Detection and Switching Strategy

Automatic detection via browser navigator.language with manual override:

// Auto-detect locale from browser settings
const detectLocale = (): string => {
  const browserLang = navigator.language;
  const supportedLocales = ['en', 'es', 'fr', 'de', 'ja'];
  
  // Match against supported locales
  const matched = supportedLocales.find(locale => 
    locale === browserLang || locale === browserLang.split('-')[0]
  );
  
  return matched || 'en';
};

// User preference override via settings
const useLocale = () => {
  const [userLocale, setUserLocale] = useState<string>(detectLocale());
  
  const switchLocale = (newLocale: string) => {
    setUserLocale(newLocale);
    localStorage.setItem('user-locale', newLocale);
  };
  
  return { locale: userLocale, switchLocale };
};

Translation File Organization

src/locales/
├── en.json
├── es.json
├── fr.json
├── de.json
└── ja.json

Each file contains translation keys with values in respective languages, maintaining consistent structure across all locales.

i18n Implementation Pattern

Using react-i18next with custom hook for consistent translation access:

// Custom i18n hook
const useTranslation = () => {
  const { t, i18n } = useTranslation();
  
  const translate = (key: string, options?: any) => {
    return t(key, options);
  };
  
  const changeLanguage = (lng: string) => {
    i18n.changeLanguage(lng);
  };
  
  return { translate, changeLanguage, currentLang: i18n.language };
};

Bidirectional Text Handling

For content that may contain mixed directionality:

  • Using dir="auto" on content containers
  • Implementing proper text alignment logic in rich text editor
  • Supporting RTL-aware selection and cursor positioning

Language Switching UX

// Language selector dropdown component
const LanguageSelector = () => {
  const { translate, currentLang, changeLanguage } = useTranslation();
  const [isOpen, setIsOpen] = useState(false);
  
  const languages = [
    { code: 'en', name: 'English' },
    { code: 'es', name: 'Español' },
    { code: 'fr', name: 'Français' },
    { code: 'de', name: 'Deutsch' },
    { code: 'ja', name: '日本語' }
  ];
  
  return (
    <div className="language-selector">
      <button onClick={() => setIsOpen(!isOpen)}>
        {languages.find(l => l.code === currentLang)?.name}
      </button>
      
      {isOpen && (
        <ul>
          {languages.map(lang => (
            <li key={lang.code} onClick={() => {
              changeLanguage(lang.code);
              setIsOpen(false);
            }}>
              {lang.name}
            </li>
          ))}
        </ul>
      )}
    </div>
  );
};

i18n in Rich Text Editor Context

Special handling for text formatting and content that may require different treatment in various languages:

  • Right-to-left text rendering
  • Different paragraph alignment rules per language
  • Proper handling of lists and numbered content in RTL languages

12. Monitoring & Observability

Real User Monitoring (RUM) Metrics to Track

The presentation editor will implement comprehensive RUM tracking to monitor user experience and performance:

  • Largest Contentful Paint (LCP): Target <2.5s for content rendering, with alerts for >4s
  • First Input Delay (FID): Target <100ms for interactive responsiveness, with alerts for >300ms
  • Cumulative Layout Shift (CLS): Target <0.1 for visual stability, with alerts for >0.25
  • Session Duration: Track average session length to identify engagement patterns
  • Feature Usage Frequency: Monitor slide creation, text editing, and media insertion rates
  • Error Rate: Track client-side errors per session with thresholds for automatic alerts

Error Tracking Integration

Sentry will be the primary error tracking solution with:

  • Automatic capture of unhandled promise rejections and synchronous errors
  • Context-aware error grouping by component, user, and session ID
  • Performance breadcrumbs for debugging complex user flows
  • Custom error tagging for presentation-specific error types (e.g., "slide-save-failure", "media-upload-error")

Custom Events and User Journey Tracking

Key user journey events to track:

  • slide.created: When a new slide is added (with slide type and content count)
  • slide.edited: When slide content is modified (with edit type: text, image, etc.)
  • presentation.saved: When presentation is saved (with save method and success/failure)
  • media.uploaded: When media files are uploaded (with file type, size, and processing time)
  • presentation.exported: When presentation is exported (with format and success status)

Performance Monitoring and Regression Detection

  • Bundle size monitoring: Track JS bundle growth with automated alerts for >10% increases
  • API response time tracking: Monitor average response times with 95th percentile thresholds
  • Memory usage monitoring: Track memory consumption for large presentations (target <100MB for 50-slide presentations)
  • Regressions detection: Automated baseline testing using performance benchmarks from previous versions
  • User session health scores: Combine multiple metrics into single health score for proactive issue detection
sequenceDiagram
    participant User
    participant Editor
    participant Sentry
    participant PerformanceMonitor
    
    User->>Editor: Create new slide
    Editor->>PerformanceMonitor: Track LCP, CLS
    Editor->>Sentry: Capture error if any
    Editor->>Editor: Log slide.created event
    
    User->>Editor: Edit content
    Editor->>PerformanceMonitor: Track FID, LCP
    Editor->>Sentry: Capture error if any
    Editor->>Editor: Log slide.edited event
    
    User->>Editor: Save presentation
    Editor->>PerformanceMonitor: Track save performance
    Editor->>Sentry: Capture error if any
    Editor->>Editor: Log presentation.saved event

Integration with Observability Platform

All metrics will be aggregated through a centralized observability platform that:

  • Provides real-time dashboards for engineering and product teams
  • Enables alerting based on custom thresholds and anomaly detection
  • Supports query capabilities for historical analysis and trend identification
  • Integrates with existing monitoring infrastructure (Datadog, New Relic) for unified view

Data Privacy Considerations

All monitoring data will:

  • Exclude any sensitive user content or presentation data
  • Comply with GDPR and CCPA requirements for user consent
  • Implement opt-out mechanisms for users who prefer not to participate in monitoring
  • Use anonymized session IDs and aggregate data where possible to protect user privacy

13. File & Folder Structure

Proposed Directory Structure

/src
├── /components
│   ├── /presentation-editor
│   │   ├── EditorContainer.tsx
│   │   ├── SlideDeck.tsx
│   │   ├── SlideEditor.tsx
│   │   ├── Toolbar.tsx
│   │   └── /elements
│   │       ├── TextElement.tsx
│   │       ├── ImageElement.tsx
│   │       └── ShapeElement.tsx
├── /features
│   ├── /presentation
│   │   ├── index.ts
│   │   ├── types.ts
│   │   ├── hooks.ts
│   │   └── store.ts
├── /hooks
│   ├── useEditorState.ts
│   └── useSlideOperations.ts
├── /utils
│   ├── domUtils.ts
│   ├── elementUtils.ts
│   └── validation.ts
├── /types
│   ├── presentation.ts
│   └── element.ts
└── /services
    ├── api.ts
    └── storage.ts

Module Boundaries and Import Rules

  • Feature isolation: Presentation editor is a self-contained feature with no cross-feature imports
  • Barrel exports: Each feature directory has an index.ts for clean imports
  • Shared components: Only shared UI primitives (buttons, inputs) are in /components/shared
  • Type safety: All feature types are strictly typed and not leaked to other features

Shared vs Feature-Specific Code

  • Feature-specific: All presentation editor logic, components, and state management live within /features/presentation
  • Shared: Common utilities like DOM helpers and validation functions in /utils with no feature-specific dependencies
  • Reusability: Components like Toolbar and Element base classes are designed to be reusable across different presentation types

This structure follows a feature-based organization that scales well for large teams while maintaining clear module boundaries and preventing circular dependencies. The component hierarchy is flat enough to avoid deep nesting while keeping related functionality grouped together.

14. Dependencies & Third-Party Libraries

Key Libraries Selected with Justification

1. React + React DOM

  • Justification: Core framework for building the UI with component-based architecture and JSX
  • Version: ^18.2.0 (stable, well-maintained)
  • Bundle size impact: Minimal, core dependency

2. TypeScript

  • Justification: Essential for type safety in large-scale frontend applications, catching runtime errors at compile time
  • Version: ^5.0.0 (latest stable with excellent IDE support)
  • Bundle size impact: None (compile-time only)

3. React Router DOM

  • Justification: Declarative routing for SPA navigation, nested routes, and route-based code splitting
  • Version: ^6.14.0 (v6 with hooks support, stable)
  • Bundle size impact: ~20KB (minimal overhead)

4. Zustand

  • Justification: Lightweight state management solution with minimal boilerplate, ideal for complex UI state like presentation editing
  • Version: ^4.3.0 (lightweight, easy to test)
  • Bundle size impact: ~2KB

5. Tailwind CSS + PostCSS

  • Justification: Utility-first CSS framework for rapid UI development with consistent design system
  • Version: ^3.3.0 (stable, extensive documentation)
  • Bundle size impact: ~15KB (configurable via purge)

6. React Markdown

  • Justification: For rendering markdown content in presentation slides with proper security and formatting
  • Version: ^8.0.0 (active, secure)
  • Bundle size impact: ~5KB

7. React Quill

  • Justification: Rich text editor for slide content editing with WYSIWYG capabilities and extensive formatting options
  • Version: ^2.0.0 (modern, actively maintained)
  • Bundle size impact: ~35KB

8. React DnD (Drag and Drop)

  • Justification: For reordering slides and drag-and-drop slide creation in presentation editor
  • Version: ^16.0.0 (stable, well-documented)
  • Bundle size impact: ~15KB

9. React Virtualized / react-window

  • Justification: For efficient rendering of large slide lists and preview panes without performance degradation
  • Version: ^1.0.0 (high-performance virtualization)
  • Bundle size impact: ~5KB

10. Axios

  • Justification: Robust HTTP client for API communication with request/response interceptors and error handling
  • Version: ^1.4.0 (well-maintained, feature-rich)
  • Bundle size impact: ~15KB

11. React Query

  • Justification: Server state management with caching, background updates, and automatic refetching for presentation data
  • Version: ^5.0.0 (latest major version with improved performance)
  • Bundle size impact: ~25KB

12. Framer Motion

  • Justification: For smooth animations and transitions in slide previews, editor interactions, and UI feedback
  • Version: ^10.0.0 (high-performance, declarative animations)
  • Bundle size impact: ~20KB

Alternatives Considered and Rejected

1. Redux Toolkit vs Zustand

  • Rejected: Zustand chosen for its simplicity and less boilerplate in a modern React context where we don't need Redux's complex middleware ecosystem

2. Material UI vs Tailwind CSS

  • Rejected: Tailwind chosen for its utility-first approach and better control over design system customization without heavy component dependencies

3. Draft.js vs React Quill

  • Rejected: React Quill chosen for better out-of-the-box formatting options, more active maintenance, and easier integration with existing rich text workflows

4. React Virtualized vs react-window

  • Rejected: react-window chosen as more actively maintained and better documented for modern React applications

Licensing and Bundle Size Impact Assessment

License Compliance: All libraries are MIT or BSD-3-Clause licensed, suitable for commercial use with proper attribution in documentation.

Bundle Size Budget Allocation:

  • Core Framework: ~50KB (React + DOM + Router)
  • State Management: ~2KB (Zustand)
  • UI Components: ~100KB (Tailwind + Quill + Motion)
  • Data Fetching & State: ~40KB (React Query + Axios)
  • Performance Optimization: ~20KB (Virtualization + DnD)
  • Total Estimated Bundle Size: ~235KB (excluding vendor libraries)

Optimization Strategy:

  • Tree-shaking enabled for all libraries
  • Code splitting via React.lazy and route-based loading
  • Bundle analyzer used to monitor growth during development

Third-Party Integration Considerations

API Client: Axios provides clean request interception and error handling for all backend communication Testing: Jest + React Testing Library for unit testing, Playwright for E2E testing Monitoring: Sentry integration planned for error tracking and performance monitoring Build Tooling: Vite or Webpack with proper configuration for optimal bundling and tree-shaking

15. Open Questions, Risks & Trade-offs

Decisions that need product/engineering alignment

  • Feature scope vs. complexity trade-off: Should we support rich text formatting (bold, italic, lists) or focus on basic text and media? The former significantly increases complexity in content modeling, editing, and rendering.
  • Real-time collaboration vs. version control: Do we prioritize real-time editing with conflict resolution or implement a simpler version-controlled approach? Real-time features require WebSocket infrastructure and complex CRDT handling.
  • Export formats: Should we support multiple export formats (PDF, PPTX, HTML)? Each format requires different rendering engines and adds significant complexity to the editor core.

Known trade-offs made in this design

  • Performance vs. feature richness: Prioritized lightweight rendering and minimal DOM manipulation over extensive formatting capabilities to maintain optimal LCP and INP scores.
  • State management simplicity vs. scalability: Used React Context API with Zustand as a fallback for complex state, rather than full Redux Toolkit to reduce boilerplate but limit scalability.
  • Component reusability vs. specificity: Created highly specific components for presentation elements (Slide, SlideContent) rather than generic component libraries to ensure consistent behavior and performance.

Risks and mitigations

  • ContentEditable instability: ContentEditable has inconsistent behavior across browsers. Mitigation includes extensive cross-browser testing and fallback to custom rich text editors for critical features.
  • Memory leaks in large presentations: Large slides with many elements could cause memory issues. Mitigation includes virtualization for large lists, proper cleanup of event listeners, and component unmounting strategies.
  • Offline editing limitations: If offline support is needed, complex state synchronization becomes a major risk. Mitigation includes optimistic updates with proper conflict resolution strategies.

Technical debt acknowledged

  • Limited accessibility support: Basic keyboard navigation and ARIA roles are implemented but comprehensive A11y compliance (WCAG 2.1 AA) requires significant additional effort that may not be prioritized in MVP.
  • Hardcoded design system: The current design system is tightly coupled to specific color schemes and typography, making future theming difficult without refactoring.
  • Limited content type extensibility: The current architecture assumes specific content types (text, image, video) and doesn't easily support custom content types or plugins.
  • No collaborative editing: Real-time collaboration features (conflict resolution, user presence indicators) are not implemented in this design and would require significant architectural changes to support.
  • Basic state normalization: While the state management approach handles most use cases, more complex scenarios (e.g., undo/redo with multiple concurrent changes) may require CRDT-based state management or more sophisticated normalization strategies.

Was this article helpful?

Topics in this article

More from Frontend LLD

  • Frontend LLD

    Whiteboard Application Low-Level Design

    Comprehensive technical design for a collaborative whiteboard application with real-time drawing, multi-user support, and scalable architecture.

  • Frontend LLD

    Notification Panel Design System

    Comprehensive low-level design for a scalable, accessible notification panel component with real-time updates and user preferences.

  • Frontend HLD

    Fast Initial Page Load Application Design

    Comprehensive frontend high-level design for optimizing application startup performance with code splitting, caching, and preloading strategies

Stay in the Loop

Get curated DSA patterns, coding problems, frontend and backend LLD/HLD, and AI/ML System Design insights delivered straight to your inbox. No spam, just focused interview and engineering content.