literate programming in the editor.jsx

This commit is contained in:
2024-12-06 18:04:27 +00:00
parent 1ba29ce6cd
commit 98cd232016

View File

@@ -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,171 +98,494 @@ 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:
// <input value={tagInput} onChange={e => 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
// 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
/**
* 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 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
* Post Saving System
*
* Handles both creating new posts and updating existing ones.
* 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.
*
* Technical Design:
* 1. State Management:
* - Sets saving flag for UI feedback
* - Clears previous errors before attempt
* - Updates UI state after completion
* Core Responsibilities:
* 1. State Synchronization:
* - Manages UI feedback states
* - Handles optimistic updates
* - Maintains data consistency
*
* 2. Data Preparation:
* - Constructs API payload
* - Handles publication date logic
* - Manages tags format
* 2. Data Processing:
* - Validates input data
* - Transforms data for API
* - Handles date/time calculations
*
* 3. API Integration:
* - Determines POST vs PUT based on ID presence
* 3. API Communication:
* - Manages HTTP requests
* - Handles authentication
* - Processes response
* - Processes API responses
*
* @param {string} newStatus - Target status ('draft' or 'published')
* @throws {Error} When API request fails
* 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<void>} 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
/**
* 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
// Prepare API Payload
/**
* 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
// Core Content Fields
title, // Post title (string)
content, // Markdown content (string)
// 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
// Tags Array
tags // Array<string>
};
try {
// API Request Configuration
/**
* 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')}`
};
// Determine API Action:
// - PUT request for existing posts (id present)
// - POST request for new posts (no id)
/**
* 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,
@@ -270,15 +593,42 @@ export default function Editor() {
);
}
// 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,6 +813,7 @@ export default function Editor() {
{/* Title Input
Full-width input for post title
Updates state directly on change
*/}
<input
type="text"
placeholder="Post title"