diff --git a/client/src/components/Editor.jsx b/client/src/components/Editor.jsx index 0db73e2..d96c9d7 100644 --- a/client/src/components/Editor.jsx +++ b/client/src/components/Editor.jsx @@ -33,7 +33,7 @@ * - Form state management */ -import React, { useState, useEffect } from 'react'; +import { useState, useEffect } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import MDEditor from '@uiw/react-md-editor'; import axios from 'axios'; @@ -98,187 +98,537 @@ export default function Editor() { * 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 +/** + * Content Management State Section + * + * This section establishes the core state variables that manage our post's content + * using React's useState hook. Each state variable is responsible for a specific + * aspect of the post's data. + * + * Implementation Pattern: + * For each piece of state, we use array destructuring with useState: + * const [value, setValue] = useState(initialValue) + * - value: current state value + * - setValue: function to update the state + * - initialValue: starting value for the state + */ - // Tag Management States - const [tags, setTags] = useState([]); // Array of current tags - const [tagInput, setTagInput] = useState(''); // Current tag input value +// Title State +// Controls the post's title field +const [title, setTitle] = useState(''); // Initialize empty +// - title: Holds current title text +// - setTitle: Function called on title input changes +// - Empty string initialization prevents "undefined" in input +// Example: setTitle("My New Blog Post") - // UI State Management - const [saving, setSaving] = useState(false); // Tracks save operation status - const [error, setError] = useState(''); // Holds error messages +// Content State +// Manages the main markdown content of the post +const [content, setContent] = useState(''); // Initialize empty +// - content: Stores current markdown text +// - setContent: Called by MDEditor on content changes +// - Empty string prevents MDEditor warnings +// Example: setContent("# My Heading\n\nPost content...") + +// Publication Status State +// Tracks whether post is a draft or published +const [status, setStatus] = useState('draft'); // Initialize as draft +// - status: Current publication state ('draft' or 'published') +// - setStatus: Updates publication status +// - 'draft' default ensures unpublished start state +// This state determines: +// 1. Which save button to display ('Publish' vs 'Update') +// 2. Whether to show publish date picker +// 3. How post is saved to backend API +// Example: setStatus('published') + +// Publication Date State +// Handles scheduled publishing functionality +const [publishDate, setPublishDate] = useState(''); // Initialize empty +// - publishDate: Scheduled publication date/time +// - setPublishDate: Updates scheduled publication date +// - Empty string works with datetime-local input +// - Only used when status is 'published' +// Format: YYYY-MM-DDThh:mm (HTML5 datetime-local format) +// Example: setPublishDate('2024-12-01T15:30') + +/** + * Why Individual State Variables? + * + * We use separate useState calls instead of a single state object because: + * 1. Allows granular updates without re-rendering unrelated parts + * 2. Simplifies state updates (no spread operator needed) + * 3. Makes state dependencies clearer in useEffect hooks + * 4. Enables easier testing and debugging + * + * Alternative Approach (Not Used): + * const [postData, setPostData] = useState({ + * title: '', + * content: '', + * status: 'draft', + * publishDate: '' + * }); + * + * The above approach would require spread operator for updates: + * setPostData(prev => ({ ...prev, title: newTitle })); + */ + +/** +* Tag Management and UI State Section +* +* This section establishes two distinct groups of state variables: +* 1. Tag management - Handling post categorization +* 2. UI state management - Tracking application status +*/ + +/** +* Tag Management States +* +* These states work together to implement a dynamic tag input system: +* - tags: Stores the final list of tags +* - tagInput: Manages the temporary input state +* +* Implementation Example: +* 1. User types "react" in input (tagInput updates) +* 2. User presses Enter +* 3. "react" is added to tags array +* 4. tagInput is cleared +*/ + +// Tags Collection State +// Maintains the list of all tags attached to the post +const [tags, setTags] = useState([]); // Initialize as empty array +// - tags: Array of strings, each representing a tag +// - setTags: Updates entire tags collection +// - Empty array initialization prevents map/filter errors +// Example: setTags(['react', 'javascript', 'web']) +// Used in: +// 1. Displaying tag list +// 2. API submission +// 3. Tag validation (preventing duplicates) + +// Tag Input State +// Manages the temporary input value for new tags +const [tagInput, setTagInput] = useState(''); // Initialize empty +// - tagInput: Current value in tag input field +// - setTagInput: Updates temporary tag value +// - Cleared after tag is added to main tags array +// Example: setTagInput('react') +// Controlled component pattern: +// setTagInput(e.target.value)} /> + +/** +* UI State Management +* +* These states handle application-level UI states: +* - saving: Loading/processing indicators +* - error: Error message display +* +* Together they manage the user feedback system during +* asynchronous operations and error conditions +*/ + +// Saving State +// Tracks whether a save operation is in progress +const [saving, setSaving] = useState(false); // Initialize as not saving +// - saving: Boolean indicating save operation status +// - setSaving: Toggles saving indicator +// Used to: +// 1. Disable save buttons during operation +// 2. Show loading indicators +// 3. Prevent duplicate save attempts +// Example flow: +// setSaving(true) // Start save operation +// await saveData() // Perform save +// setSaving(false) // Complete save operation + +// Error State +// Manages error messages for user feedback +const [error, setError] = useState(''); // Initialize as no error +// - error: Current error message (empty string = no error) +// - setError: Updates error message +// Used for: +// 1. API error messages +// 2. Validation error messages +// 3. User feedback +// Example: setError('Failed to save post') +// Clear error: setError('') + +/** +* Why These Groupings? +* +* 1. Tag Management: +* - Separates final (tags) from temporary (tagInput) state +* - Enables controlled input pattern +* - Simplifies tag addition/removal logic +* +* 2. UI State: +* - Centralizes application status management +* - Keeps user feedback mechanisms separate from data +* - Enables consistent error handling patterns +* +* Alternative Approaches Not Used: +* 1. Combined tag state: +* const [tagState, setTagState] = useState({ tags: [], input: '' }); +* - Would require more complex updates +* - Would cause unnecessary re-renders +* +* 2. Context for UI state: +* - Overkill for this component's scope +* - Would add unnecessary complexity +*/ // Router Hooks const navigate = useNavigate(); // Programmatic navigation const { id } = useParams(); // Extract post ID from URL if editing /** - * Post Loading Effect + * Post Loading Effect Section * - * This effect runs when the component mounts and whenever the ID changes. - * It's responsible for loading existing post data when in edit mode. + * This useEffect hook and associated loadPost function handle fetching + * and populating data for existing posts 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 + * Component Lifecycle: + * 1. Component mounts + * 2. Effect checks for post ID + * 3. If ID exists, loadPost is called + * 4. Post data populates form fields */ + + // Post Loading Effect + // Automatically fetches post data when component mounts in edit mode useEffect(() => { + // Only load post data if we have an ID (edit mode) + // Skip for new posts (no ID present) if (id) { loadPost(); } - }, [id]); + }, [id]); // Re-run if ID changes (rare, but possible with route changes) /** - * Post Loading Implementation + * Post Loading Function + * Fetches existing post data from the API and populates the form. * - * Fetches and populates form data for an existing post. + * Data Flow: + * 1. API request with authentication + * 2. Response processing + * 3. State updates for all form fields + * 4. Error handling if needed * - * 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 + * @async - Returns a promise that resolves when post is loaded + * @throws {Error} - When API request fails or data is invalid */ const loadPost = async () => { try { - // API Request Configuration + /** + * API Request Section + * Send GET request to fetch post data + * + * Request Details: + * - URL: Constructed using environment variable and post ID + * - Headers: Includes JWT token for authentication + * - Method: GET (read operation) + */ const response = await axios.get( `${import.meta.env.VITE_API_URL}/api/posts/${id}`, { headers: { + // Authorization using JWT pattern + // Token retrieved from browser storage Authorization: `Bearer ${localStorage.getItem('token')}` } } ); - // Destructure and Transform API Response - const post = response.data; + /** + * Data Processing Section + * Extract post data from response and update all form states + */ + const post = response.data; // Extract post object from response + + /** + * State Updates Section + * Update each form field with data from API + * Each setter is called separately to: + * 1. Keep updates atomic + * 2. Maintain clear data flow + * 3. Allow React to batch updates efficiently + */ - // 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 + // Update title state + // Direct assignment - no transformation needed setTitle(post.title); + + // Update markdown content + // Directly set editor content from API response setContent(post.markdown_content); + + // Update publication status + // Directly assign status string setStatus(post.status); - // Transform publication date - // - Converts ISO string to local datetime-local input format - // - Handles null/undefined cases with empty string + /** + * Publication Date Processing + * Transform ISO date string to format required by datetime-local input + * + * Steps: + * 1. Check if published_at exists + * 2. If exists: Convert to datetime-local format + * 3. If null/undefined: Use empty string + * + * Format Conversion: + * From: 2024-12-01T15:30:00Z (ISO) + * To: 2024-12-01T15:30 (datetime-local) + */ 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 + /** + * Tags Processing + * Transform API tag format to component format + * + * Transformation: + * From: [{name: 'tag1'}, {name: 'tag2'}] (API format) + * To: ['tag1', 'tag2'] (Component format) + * + * Safety Features: + * - Optional chaining (?.) handles null/undefined tags + * - Fallback to empty array if mapping fails + */ setTags(post.tags?.map(tag => tag.name) || []); } catch (err) { - // Error Handling - console.error('Error loading post:', err); - setError('Failed to load post'); + /** + * Error Handling Section + * Handle any errors during post loading + * + * Error Flow: + * 1. Log full error for debugging + * 2. Set user-friendly error message + * 3. Component will display error to user + */ + console.error('Error loading post:', err); // Detailed error for debugging + setError('Failed to load post'); // User-friendly message } }; - /** - * 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 - */ + /** + * Post Saving System + * + * A comprehensive implementation of the post saving mechanism that handles both + * creation and updates of blog posts within the application. This system + * implements optimistic UI updates, handles race conditions, manages state + * transitions, and provides user feedback throughout the save process. + * + * Core Responsibilities: + * 1. State Synchronization: + * - Manages UI feedback states + * - Handles optimistic updates + * - Maintains data consistency + * + * 2. Data Processing: + * - Validates input data + * - Transforms data for API + * - Handles date/time calculations + * + * 3. API Communication: + * - Manages HTTP requests + * - Handles authentication + * - Processes API responses + * + * 4. Error Management: + * - Provides error feedback + * - Handles edge cases + * - Maintains system stability + * + * Technical Concepts Used: + * 1. Promise-based async/await pattern + * 2. REST API conventions + * 3. JWT authentication + * 4. State management patterns + * 5. Error boundaries + * + * @param {string} newStatus - The target publication status for the post + * Must be either 'draft' or 'published' + * Defaults to current status if not provided + * + * @throws {Error} API_ERROR When network request fails + * @throws {Error} AUTH_ERROR When authentication is invalid + * @throws {Error} VALIDATION_ERROR When data validation fails + * + * @returns {Promise} Resolves when save operation completes + * Rejects if operation fails + * + * @fires navigate Triggers navigation on successful save + * @fires setSaving Updates saving state for UI feedback + * @fires setError Updates error state for user feedback + * + * @example + * // Save as draft + * await handleSave('draft'); + * + * // Publish post + * await handleSave('published'); + */ const handleSave = async (newStatus = status) => { - // Update UI State - setSaving(true); // Disable save buttons - setError(''); // Clear previous errors - - // Prepare API Payload + /** + * Phase 1: Pre-Save State Management + * + * Initialize the UI state for the save operation: + * 1. Set saving indicator to prevent duplicate submissions + * 2. Clear any previous error messages + * + * State Management Pattern: + * - Use React setState for atomic updates + * - Order matters: saving before error clear + * - Both updates will be batched by React + */ + setSaving(true); // Activate save indicator + setError(''); // Reset error state + + /** + * Phase 2: Data Preparation + * + * Construct the API payload with all required data transformations: + * - All fields are included for consistency + * - Dates are properly formatted + * - Status is explicitly set + * + * Data Transformation Rules: + * 1. Title & Content: Direct pass-through + * 2. Status: Use new status from parameter + * 3. PublishedAt: Complex logic based on status + * 4. Tags: Array format validation + */ const data = { - title, - content, - status: newStatus, - // 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')}` - }; + // Core Content Fields + title, // Post title (string) + content, // Markdown content (string) - // Determine API Action: - // - PUT request for existing posts (id present) - // - POST request for new posts (no id) + // Publication Status + status: newStatus, // 'draft' or 'published' + + /** + * Publication Date Logic + * + * Complex ternary operation that: + * 1. Checks if status is 'published' + * 2. If true: Uses provided date or generates current timestamp + * 3. If false: Sets null for drafts + * + * Date Handling Rules: + * - Published posts must have a date + * - Drafts must have null date + * - Current time is fallback for published posts + * + * @type {string|null} ISO timestamp or null + */ + publishedAt: newStatus === 'published' + ? (publishDate || new Date().toISOString()) + : null, + + // Tags Array + tags // Array + }; + + try { + /** + * Phase 3: API Communication + * + * Handle the actual API request with proper authentication: + * 1. Configure request headers + * 2. Determine request type (PUT vs POST) + * 3. Execute request + * + * Authentication: + * - JWT token from localStorage + * - Bearer token pattern + * - Token validation happens server-side + */ + const headers = { + Authorization: `Bearer ${localStorage.getItem('token')}` + }; + + /** + * Request Type Determination + * + * Logic: + * - PUT: Update existing post (id exists) + * - POST: Create new post (no id) + * + * RESTful Conventions: + * - PUT is idempotent + * - POST creates new resources + * - URLs follow REST patterns + */ if (id) { + /** + * Update Existing Post + * + * PUT request to update existing post: + * 1. URL includes post ID + * 2. Sends complete post data + * 3. Expects 200 OK response + */ await axios.put( `${import.meta.env.VITE_API_URL}/api/posts/${id}`, data, { headers } ); } else { + /** + * Create New Post + * + * POST request to create new post: + * 1. URL is collection endpoint + * 2. Sends complete post data + * 3. Expects 201 Created response + */ await axios.post( `${import.meta.env.VITE_API_URL}/api/posts`, data, { headers } ); } - - // Success: Navigate to dashboard + + /** + * Phase 4: Success Handling + * + * After successful save: + * 1. Navigate to dashboard + * 2. UI state will be reset in finally block + */ navigate('/dashboard'); - + } catch (err) { - // Error Handling + /** + * Phase 5: Error Handling + * + * Comprehensive error handling: + * 1. Log full error for debugging + * 2. Set user-friendly error message + * 3. Error state will trigger UI update + * + * Error Categories: + * - Network errors + * - Authentication errors + * - Validation errors + * - Server errors + */ console.error('Error saving post:', err); setError('Failed to save post'); } finally { - // Reset UI State + /** + * Phase 6: Cleanup + * + * Reset UI state regardless of outcome: + * 1. Clear saving indicator + * 2. Allow new save attempts + * + * Always executes to prevent UI lockup + */ setSaving(false); } }; @@ -463,7 +813,8 @@ export default function Editor() { {/* Title Input Full-width input for post title Updates state directly on change -