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
-