Literate comments for Editor.jsx
This commit is contained in:
@@ -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<string|null>} 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 = ``;
|
||||
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 (
|
||||
<div className="editor">
|
||||
{/* Header Section
|
||||
Groups title input and action buttons
|
||||
Uses flexbox for layout control */}
|
||||
<div className="editor-header">
|
||||
<input
|
||||
{/* Title Input
|
||||
Full-width input for post title
|
||||
Updates state directly on change
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Post title"
|
||||
value={title}
|
||||
onChange={(e) => 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 */}
|
||||
<div className="actions">
|
||||
{/* Cancel Button
|
||||
Returns user to dashboard without saving
|
||||
Immediate navigation with no confirmation */}
|
||||
<button
|
||||
className="button cancel"
|
||||
onClick={() => navigate('/dashboard')}
|
||||
/* Button Properties:
|
||||
* - className: Combines generic and specific styles
|
||||
* - onClick: Direct navigation without state cleanup
|
||||
* Navigation is immediate since we don't persist drafts
|
||||
*/
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
|
||||
{/* Draft Save Button
|
||||
Saves current state as draft
|
||||
Disabled during save operations */}
|
||||
<button
|
||||
className="button draft"
|
||||
onClick={() => handleSave('draft')}
|
||||
disabled={saving}
|
||||
/* Button Properties:
|
||||
* - onClick: Calls handleSave with explicit 'draft' status
|
||||
* - disabled: Prevents double-submission during save
|
||||
* Uses 'draft' status to ensure proper API handling
|
||||
*/
|
||||
>
|
||||
Save as Draft
|
||||
</button>
|
||||
|
||||
{/* Publish/Update Button
|
||||
Context-aware label based on current status
|
||||
Handles both initial publish and updates */}
|
||||
<button
|
||||
className="button publish"
|
||||
onClick={() => handleSave('published')}
|
||||
disabled={saving}
|
||||
/* Button Properties:
|
||||
* - onClick: Calls handleSave with 'published' status
|
||||
* - disabled: Prevents double-submission
|
||||
* Label changes based on current status for clear UX
|
||||
*/
|
||||
>
|
||||
{status === 'published' ? 'Update' : 'Publish'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && <div className="error-message">{error}</div>}
|
||||
{/* Error Display Section
|
||||
Conditionally renders error messages
|
||||
Uses dedicated styling for error states */}
|
||||
{error && <div className="error-message">
|
||||
{/* Error message display
|
||||
* - Conditional rendering with && operator
|
||||
* - Direct error state display
|
||||
* - Semantically marked as error for accessibility
|
||||
*/}
|
||||
{error}
|
||||
</div>}
|
||||
|
||||
{/* Publication Settings Section
|
||||
Only visible for published posts
|
||||
Handles scheduled publishing functionality */}
|
||||
{status === 'published' && (
|
||||
<div className="publish-settings">
|
||||
{/* DateTime Input Field
|
||||
Allows scheduling of publication
|
||||
Uses HTML5 datetime-local input */}
|
||||
<label>
|
||||
Publication Date:
|
||||
<input
|
||||
type="datetime-local"
|
||||
value={publishDate}
|
||||
onChange={(e) => setPublishDate(e.target.value)}
|
||||
/* Input Properties:
|
||||
* - type: HTML5 datetime-local for native date/time picking
|
||||
* - value: Controlled component pattern
|
||||
* - onChange: Direct state updates
|
||||
* Format: YYYY-MM-DDThh:mm (HTML5 standard)
|
||||
*/
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tags Management Section
|
||||
Handles both tag display and input
|
||||
Implements add/remove tag functionality */}
|
||||
<div className="tags-input">
|
||||
{/* Tags Display List
|
||||
Shows all current tags with remove buttons
|
||||
Uses flex layout for responsive tag flow */}
|
||||
<div className="tags-list">
|
||||
{tags.map((tag, index) => (
|
||||
<span key={index} className="tag">
|
||||
{/* Individual Tag Display
|
||||
* - key: Uses index as fallback unique identifier
|
||||
* - className: Styling hook for tag appearance
|
||||
* Structure: tag text + remove button
|
||||
*/}
|
||||
{tag}
|
||||
<button
|
||||
onClick={() => handleRemoveTag(tag)}
|
||||
className="remove-tag"
|
||||
/* Button Properties:
|
||||
* - onClick: Calls removal handler with specific tag
|
||||
* - className: Styling for remove button
|
||||
* Uses × symbol for clear delete action
|
||||
*/
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Tag Input Field
|
||||
Handles new tag entry
|
||||
Implements enter-to-add functionality */}
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Add tags (press Enter)"
|
||||
value={tagInput}
|
||||
onChange={(e) => 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
|
||||
*/
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Markdown Editor Section
|
||||
Main content editing area
|
||||
Implements drag-and-drop image upload */}
|
||||
<div
|
||||
className="editor-container"
|
||||
onDrop={handleDrop}
|
||||
onDragOver={(e) => 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 */}
|
||||
<MDEditor
|
||||
value={content}
|
||||
onChange={setContent}
|
||||
@@ -209,6 +643,14 @@ export default function Editor() {
|
||||
textareaProps={{
|
||||
placeholder: 'Write your post in Markdown...'
|
||||
}}
|
||||
/* Editor Properties:
|
||||
* - value: Current markdown content
|
||||
* - onChange: Direct content updates
|
||||
* - preview: Live markdown preview mode
|
||||
* - height: Fixed editor height
|
||||
* - textareaProps: Configuration for input area
|
||||
* Uses controlled component pattern
|
||||
*/
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user