From 1ba29ce6cd80789d7101da2c5bece185427879b5 Mon Sep 17 00:00:00 2001 From: rishi Date: Sun, 1 Dec 2024 18:00:36 +0000 Subject: [PATCH] Literate comments for Editor.jsx --- client/src/components/Editor.jsx | 498 +++++++++++++++++++++++++++++-- 1 file changed, 470 insertions(+), 28 deletions(-) diff --git a/client/src/components/Editor.jsx b/client/src/components/Editor.jsx index 535d323..0db73e2 100644 --- a/client/src/components/Editor.jsx +++ b/client/src/components/Editor.jsx @@ -1,87 +1,327 @@ -// src/components/Editor.jsx +/** + * @fileoverview Editor Component - A comprehensive blog post editor implementation + * + * This file implements a full-featured Markdown blog post editor using React hooks and modern web APIs. + * It serves as an example of several important React patterns and concepts: + * + * 1. Complex Form State Management: + * - Multiple interconnected form fields + * - Derived state calculations + * - Form validation and error handling + * + * 2. Side Effects and Data Fetching: + * - useEffect for data loading + * - API integration with axios + * - Error boundary implementation + * + * 3. File Handling: + * - Drag and drop API implementation + * - File upload with FormData + * - Image processing and markdown integration + * + * 4. State Management Patterns: + * - Multiple useState hooks for granular updates + * - Derived state calculations + * - State update batching + * + * The component demonstrates best practices for: + * - React hooks usage + * - Error handling + * - API integration + * - User input processing + * - Real-time updates + * - Form state management + */ + import React, { useState, useEffect } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import MDEditor from '@uiw/react-md-editor'; import axios from 'axios'; -export default function Editor() { - const [title, setTitle] = useState(''); - const [content, setContent] = useState(''); - const [status, setStatus] = useState('draft'); - const [publishDate, setPublishDate] = useState(''); - const [tags, setTags] = useState([]); - const [tagInput, setTagInput] = useState(''); - const [saving, setSaving] = useState(false); - const [error, setError] = useState(''); - const navigate = useNavigate(); - const { id } = useParams(); +/** + * @typedef {Object} Post - Represents a blog post in our system + * @property {string} title - The post's title + * @property {string} markdown_content - The post's content in Markdown format + * @property {('draft'|'published')} status - Current status of the post + * @property {string} published_at - ISO timestamp of publication + * @property {Array<{name: string}>} tags - Array of tag objects + * + * Understanding the Post Type: + * - title: User-provided string, no length restrictions enforced in type + * - markdown_content: Raw markdown string, can include images, code blocks, etc. + * - status: Union type restricting values to either 'draft' or 'published' + * - published_at: ISO8601 timestamp string (e.g., "2024-01-01T12:00:00Z") + * - tags: Array of objects, each with a name property + */ +/** + * Editor Component Implementation + * + * This component manages the entire editing experience for a blog post. + * It handles both creation of new posts and editing of existing ones through + * a single interface, determining its mode based on the presence of an ID parameter. + * + * State Management Overview: + * - Form Fields: title, content, status, publishDate + * - UI State: saving, error + * - Tag Management: tags, tagInput + * + * The component uses URL parameters to determine if it's editing an existing post, + * with all state initialized to empty/default values and populated via useEffect + * if an ID is present. + * + * Key Technical Concepts: + * 1. React Router Integration: + * - useNavigate: Programmatic navigation + * - useParams: URL parameter extraction + * + * 2. State Management: + * - Multiple useState hooks for granular updates + * - Derived state calculations + * - Optimistic updates + * + * 3. Side Effects: + * - Data fetching + * - Error handling + * - Cleanup + * + * @component + */ +export default function Editor() { + /** + * State Hooks Section + * + * This component uses multiple useState hooks rather than a single state object. + * This decision was made to: + * 1. Allow for granular updates without re-rendering unrelated parts + * 2. Simplify state updates by avoiding spread operator usage + * 3. Make state dependencies clearer in useEffect hooks + */ + + // Content Management States + const [title, setTitle] = useState(''); // Main post title + const [content, setContent] = useState(''); // Markdown content + const [status, setStatus] = useState('draft'); // Publication status + const [publishDate, setPublishDate] = useState(''); // Scheduled publish date + + // Tag Management States + const [tags, setTags] = useState([]); // Array of current tags + const [tagInput, setTagInput] = useState(''); // Current tag input value + + // UI State Management + const [saving, setSaving] = useState(false); // Tracks save operation status + const [error, setError] = useState(''); // Holds error messages + + // Router Hooks + const navigate = useNavigate(); // Programmatic navigation + const { id } = useParams(); // Extract post ID from URL if editing + + /** + * Post Loading Effect + * + * This effect runs when the component mounts and whenever the ID changes. + * It's responsible for loading existing post data when in edit mode. + * + * Key Concepts: + * 1. Conditional Execution: + * Only runs if an ID is present (edit mode) + * + * 2. Dependency Array: + * [id] ensures the effect reruns if the URL parameter changes + * + * 3. Error Handling: + * Errors during loading are caught and displayed to the user + */ useEffect(() => { if (id) { loadPost(); } }, [id]); + /** + * Post Loading Implementation + * + * Fetches and populates form data for an existing post. + * + * Technical Implementation Details: + * 1. Authentication: + * - Sends Bearer token in Authorization header + * - Token is retrieved from localStorage + * + * 2. Data Transformation: + * - Converts API response format to component state format + * - Handles potential missing data gracefully + * + * 3. Error Handling: + * - Catches and logs API errors + * - Updates UI error state for user feedback + * + * @async + * @throws {Error} When API request fails + */ const loadPost = async () => { try { - const response = await axios.get(`${import.meta.env.VITE_API_URL}/api/posts/${id}`, { - headers: { Authorization: `Bearer ${localStorage.getItem('token')}` } - }); + // API Request Configuration + const response = await axios.get( + `${import.meta.env.VITE_API_URL}/api/posts/${id}`, + { + headers: { + Authorization: `Bearer ${localStorage.getItem('token')}` + } + } + ); + + // Destructure and Transform API Response const post = response.data; + + // Update Component State + // Each setter is called individually to: + // 1. Maintain clear data flow + // 2. Allow React to batch updates + // 3. Keep transformations clear and separated setTitle(post.title); setContent(post.markdown_content); setStatus(post.status); - setPublishDate(post.published_at ? new Date(post.published_at).toISOString().slice(0, 16) : ''); + + // Transform publication date + // - Converts ISO string to local datetime-local input format + // - Handles null/undefined cases with empty string + setPublishDate( + post.published_at + ? new Date(post.published_at).toISOString().slice(0, 16) + : '' + ); + + // Transform tags from API format to component format + // - Maps array of objects to array of strings + // - Handles null/undefined with empty array fallback setTags(post.tags?.map(tag => tag.name) || []); + } catch (err) { + // Error Handling console.error('Error loading post:', err); setError('Failed to load post'); } }; + /** + * Post Saving Implementation + * + * Handles both creating new posts and updating existing ones. + * + * Technical Design: + * 1. State Management: + * - Sets saving flag for UI feedback + * - Clears previous errors before attempt + * - Updates UI state after completion + * + * 2. Data Preparation: + * - Constructs API payload + * - Handles publication date logic + * - Manages tags format + * + * 3. API Integration: + * - Determines POST vs PUT based on ID presence + * - Handles authentication + * - Processes response + * + * @param {string} newStatus - Target status ('draft' or 'published') + * @throws {Error} When API request fails + */ const handleSave = async (newStatus = status) => { - setSaving(true); - setError(''); + // Update UI State + setSaving(true); // Disable save buttons + setError(''); // Clear previous errors + // Prepare API Payload const data = { title, content, status: newStatus, - publishedAt: newStatus === 'published' ? (publishDate || new Date().toISOString()) : null, + // Publication Date Logic: + // 1. If publishing, use provided date or current time + // 2. If draft, set to null + publishedAt: newStatus === 'published' + ? (publishDate || new Date().toISOString()) + : null, tags }; try { + // API Request Configuration + const headers = { + Authorization: `Bearer ${localStorage.getItem('token')}` + }; + + // Determine API Action: + // - PUT request for existing posts (id present) + // - POST request for new posts (no id) if (id) { - await axios.put(`${import.meta.env.VITE_API_URL}/api/posts/${id}`, data, { - headers: { Authorization: `Bearer ${localStorage.getItem('token')}` } - }); + await axios.put( + `${import.meta.env.VITE_API_URL}/api/posts/${id}`, + data, + { headers } + ); } else { - await axios.post(`${import.meta.env.VITE_API_URL}/api/posts`, data, { - headers: { Authorization: `Bearer ${localStorage.getItem('token')}` } - }); + await axios.post( + `${import.meta.env.VITE_API_URL}/api/posts`, + data, + { headers } + ); } + + // Success: Navigate to dashboard navigate('/dashboard'); + } catch (err) { + // Error Handling console.error('Error saving post:', err); setError('Failed to save post'); } finally { + // Reset UI State setSaving(false); } }; + /** + * Image Upload Handler + * + * Manages the process of uploading images to the server and + * returning the URL for markdown insertion. + * + * Technical Implementation: + * 1. FormData Usage: + * - Constructs multipart/form-data payload + * - Handles file data properly + * + * 2. Error Handling: + * - Validates post existence + * - Handles API errors + * - Provides user feedback + * + * 3. Security: + * - Sends authentication token + * - Uses proper content type headers + * + * @param {File} file - The image file to upload + * @returns {Promise} The uploaded image URL or null if failed + */ const handleImageUpload = async (file) => { + // Validate Post Existence + // Images must be attached to an existing post if (!id) { setError('Please save the post first to upload images'); return null; } + // Prepare FormData const formData = new FormData(); formData.append('file', file); try { + // Configure API Request const response = await axios.post( - `${import.meta.env.VITE_API_URL}/api/posts/${id}/images`, + `${import.meta.env.VITE_API_URL}/api/posts/${id}/images`, formData, { headers: { @@ -90,117 +330,311 @@ export default function Editor() { } } ); + + // Return Image URL return response.data.url; } catch (err) { + // Error Handling console.error('Error uploading image:', err); setError('Failed to upload image'); return null; } }; + /** + * Drag and Drop Handler + * + * Implements the HTML5 Drag and Drop API for image uploads. + * + * Technical Details: + * 1. Event Handling: + * - Prevents default browser behavior + * - Extracts file from drop event + * + * 2. File Processing: + * - Validates file type + * - Triggers upload + * - Inserts markdown + * + * 3. Content Updates: + * - Maintains existing content + * - Adds image markdown at cursor position + * + * @param {DragEvent} event - The drop event object + */ const handleDrop = async (event) => { + // Prevent default browser handling event.preventDefault(); + + // Extract and validate file const file = event.dataTransfer.files[0]; if (file && file.type.startsWith('image/')) { + // Upload image and get URL const imageUrl = await handleImageUpload(file); if (imageUrl) { + // Create and insert markdown const imageMarkdown = `![${file.name}](${imageUrl})`; setContent(prev => prev + '\n' + imageMarkdown); } } }; + /** + * Tag Addition Handler + * + * Manages the addition of new tags to the post. + * + * Implementation Details: + * 1. Input Validation: + * - Checks for Enter key + * - Validates tag content + * - Prevents duplicates + * + * 2. State Updates: + * - Adds new tag to array + * - Clears input field + * + * @param {KeyboardEvent} e - The keyboard event object + */ const handleAddTag = (e) => { if (e.key === 'Enter' && tagInput.trim()) { + // Check for duplicate tags if (!tags.includes(tagInput.trim())) { + // Update tags array setTags([...tags, tagInput.trim()]); } + // Clear input field setTagInput(''); } }; + /** + * Tag Removal Handler + * + * Manages the removal of tags from the post. + * + * Implementation Details: + * 1. Array Filtering: + * - Creates new array without removed tag + * - Maintains tag order + * + * 2. State Update: + * - Updates tags array immutably + * + * @param {string} tagToRemove - The tag to remove + */ const handleRemoveTag = (tagToRemove) => { setTags(tags.filter(tag => tag !== tagToRemove)); }; + /** + * Component Render Method + * + * Renders the editor interface with all its features. + * The JSX is organized into logical sections: + * 1. Header with title and actions + * 2. Error display + * 3. Publication settings + * 4. Tag management + * 5. Markdown editor + * + * Key Design Decisions: + * 1. Semantic HTML: + * - Proper heading hierarchy + * - Semantic grouping + * - Accessibility considerations + * + * 2. Component Organization: + * - Logical grouping of related elements + * - Clear visual hierarchy + * - Consistent spacing + * + * 3. Event Handling: + * - Centralized handlers + * - Proper event delegation + * - Performance considerations + */ return (
+ {/* Header Section + Groups title input and action buttons + Uses flexbox for layout control */}
- setTitle(e.target.value)} className="title-input" + /* Input Properties Explained: + * - type="text": Standard text input for single-line content + * - placeholder: Provides user guidance when empty + * - value: Controlled component pattern implementation + * - onChange: Direct state updates for immediate UI feedback + * - className: Styling hook for CSS customization + */ /> + {/* Action Buttons Section + Groups all post management buttons + Uses flex container for button alignment */}
+ {/* Cancel Button + Returns user to dashboard without saving + Immediate navigation with no confirmation */} + + {/* Draft Save Button + Saves current state as draft + Disabled during save operations */} + + {/* Publish/Update Button + Context-aware label based on current status + Handles both initial publish and updates */}
- {error &&
{error}
} + {/* Error Display Section + Conditionally renders error messages + Uses dedicated styling for error states */} + {error &&
+ {/* Error message display + * - Conditional rendering with && operator + * - Direct error state display + * - Semantically marked as error for accessibility + */} + {error} +
} + {/* Publication Settings Section + Only visible for published posts + Handles scheduled publishing functionality */} {status === 'published' && (
+ {/* DateTime Input Field + Allows scheduling of publication + Uses HTML5 datetime-local input */}
)} + {/* Tags Management Section + Handles both tag display and input + Implements add/remove tag functionality */}
+ {/* Tags Display List + Shows all current tags with remove buttons + Uses flex layout for responsive tag flow */}
{tags.map((tag, index) => ( + {/* Individual Tag Display + * - key: Uses index as fallback unique identifier + * - className: Styling hook for tag appearance + * Structure: tag text + remove button + */} {tag} ))}
+ + {/* Tag Input Field + Handles new tag entry + Implements enter-to-add functionality */} setTagInput(e.target.value)} onKeyDown={handleAddTag} + /* Input Properties: + * - type: Standard text input for tag entry + * - placeholder: User instruction for tag addition + * - value: Controlled component pattern + * - onChange: Direct state updates + * - onKeyDown: Handles Enter key for tag addition + */ />
+ {/* Markdown Editor Section + Main content editing area + Implements drag-and-drop image upload */}
e.preventDefault()} + /* Container Properties: + * - className: Styling hook for editor container + * - onDrop: Handles image file drops + * - onDragOver: Prevents default drag behavior + * Implements HTML5 drag and drop API + */ > + {/* MDEditor Component + Third-party markdown editor with preview + Implements GitHub-flavored markdown */}