Literate comments for Editor.jsx
This commit is contained in:
@@ -1,85 +1,325 @@
|
|||||||
// 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 React, { useState, useEffect } from 'react';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
import MDEditor from '@uiw/react-md-editor';
|
import MDEditor from '@uiw/react-md-editor';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
export default function Editor() {
|
/**
|
||||||
const [title, setTitle] = useState('');
|
* @typedef {Object} Post - Represents a blog post in our system
|
||||||
const [content, setContent] = useState('');
|
* @property {string} title - The post's title
|
||||||
const [status, setStatus] = useState('draft');
|
* @property {string} markdown_content - The post's content in Markdown format
|
||||||
const [publishDate, setPublishDate] = useState('');
|
* @property {('draft'|'published')} status - Current status of the post
|
||||||
const [tags, setTags] = useState([]);
|
* @property {string} published_at - ISO timestamp of publication
|
||||||
const [tagInput, setTagInput] = useState('');
|
* @property {Array<{name: string}>} tags - Array of tag objects
|
||||||
const [saving, setSaving] = useState(false);
|
*
|
||||||
const [error, setError] = useState('');
|
* Understanding the Post Type:
|
||||||
const navigate = useNavigate();
|
* - title: User-provided string, no length restrictions enforced in type
|
||||||
const { id } = useParams();
|
* - 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(() => {
|
useEffect(() => {
|
||||||
if (id) {
|
if (id) {
|
||||||
loadPost();
|
loadPost();
|
||||||
}
|
}
|
||||||
}, [id]);
|
}, [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 () => {
|
const loadPost = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(`${import.meta.env.VITE_API_URL}/api/posts/${id}`, {
|
// API Request Configuration
|
||||||
headers: { Authorization: `Bearer ${localStorage.getItem('token')}` }
|
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;
|
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);
|
setTitle(post.title);
|
||||||
setContent(post.markdown_content);
|
setContent(post.markdown_content);
|
||||||
setStatus(post.status);
|
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) || []);
|
setTags(post.tags?.map(tag => tag.name) || []);
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
// Error Handling
|
||||||
console.error('Error loading post:', err);
|
console.error('Error loading post:', err);
|
||||||
setError('Failed to load post');
|
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) => {
|
const handleSave = async (newStatus = status) => {
|
||||||
setSaving(true);
|
// Update UI State
|
||||||
setError('');
|
setSaving(true); // Disable save buttons
|
||||||
|
setError(''); // Clear previous errors
|
||||||
|
|
||||||
|
// Prepare API Payload
|
||||||
const data = {
|
const data = {
|
||||||
title,
|
title,
|
||||||
content,
|
content,
|
||||||
status: newStatus,
|
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
|
tags
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
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) {
|
if (id) {
|
||||||
await axios.put(`${import.meta.env.VITE_API_URL}/api/posts/${id}`, data, {
|
await axios.put(
|
||||||
headers: { Authorization: `Bearer ${localStorage.getItem('token')}` }
|
`${import.meta.env.VITE_API_URL}/api/posts/${id}`,
|
||||||
});
|
data,
|
||||||
|
{ headers }
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
await axios.post(`${import.meta.env.VITE_API_URL}/api/posts`, data, {
|
await axios.post(
|
||||||
headers: { Authorization: `Bearer ${localStorage.getItem('token')}` }
|
`${import.meta.env.VITE_API_URL}/api/posts`,
|
||||||
});
|
data,
|
||||||
|
{ headers }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Success: Navigate to dashboard
|
||||||
navigate('/dashboard');
|
navigate('/dashboard');
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
// Error Handling
|
||||||
console.error('Error saving post:', err);
|
console.error('Error saving post:', err);
|
||||||
setError('Failed to save post');
|
setError('Failed to save post');
|
||||||
} finally {
|
} finally {
|
||||||
|
// Reset UI State
|
||||||
setSaving(false);
|
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) => {
|
const handleImageUpload = async (file) => {
|
||||||
|
// Validate Post Existence
|
||||||
|
// Images must be attached to an existing post
|
||||||
if (!id) {
|
if (!id) {
|
||||||
setError('Please save the post first to upload images');
|
setError('Please save the post first to upload images');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Prepare FormData
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Configure API Request
|
||||||
const response = await axios.post(
|
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,
|
formData,
|
||||||
@@ -90,117 +330,311 @@ export default function Editor() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Return Image URL
|
||||||
return response.data.url;
|
return response.data.url;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
// Error Handling
|
||||||
console.error('Error uploading image:', err);
|
console.error('Error uploading image:', err);
|
||||||
setError('Failed to upload image');
|
setError('Failed to upload image');
|
||||||
return null;
|
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) => {
|
const handleDrop = async (event) => {
|
||||||
|
// Prevent default browser handling
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
|
// Extract and validate file
|
||||||
const file = event.dataTransfer.files[0];
|
const file = event.dataTransfer.files[0];
|
||||||
if (file && file.type.startsWith('image/')) {
|
if (file && file.type.startsWith('image/')) {
|
||||||
|
// Upload image and get URL
|
||||||
const imageUrl = await handleImageUpload(file);
|
const imageUrl = await handleImageUpload(file);
|
||||||
if (imageUrl) {
|
if (imageUrl) {
|
||||||
|
// Create and insert markdown
|
||||||
const imageMarkdown = ``;
|
const imageMarkdown = ``;
|
||||||
setContent(prev => prev + '\n' + 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) => {
|
const handleAddTag = (e) => {
|
||||||
if (e.key === 'Enter' && tagInput.trim()) {
|
if (e.key === 'Enter' && tagInput.trim()) {
|
||||||
|
// Check for duplicate tags
|
||||||
if (!tags.includes(tagInput.trim())) {
|
if (!tags.includes(tagInput.trim())) {
|
||||||
|
// Update tags array
|
||||||
setTags([...tags, tagInput.trim()]);
|
setTags([...tags, tagInput.trim()]);
|
||||||
}
|
}
|
||||||
|
// Clear input field
|
||||||
setTagInput('');
|
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) => {
|
const handleRemoveTag = (tagToRemove) => {
|
||||||
setTags(tags.filter(tag => tag !== 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 (
|
return (
|
||||||
<div className="editor">
|
<div className="editor">
|
||||||
|
{/* Header Section
|
||||||
|
Groups title input and action buttons
|
||||||
|
Uses flexbox for layout control */}
|
||||||
<div className="editor-header">
|
<div className="editor-header">
|
||||||
|
{/* Title Input
|
||||||
|
Full-width input for post title
|
||||||
|
Updates state directly on change
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Post title"
|
placeholder="Post title"
|
||||||
value={title}
|
value={title}
|
||||||
onChange={(e) => setTitle(e.target.value)}
|
onChange={(e) => setTitle(e.target.value)}
|
||||||
className="title-input"
|
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">
|
<div className="actions">
|
||||||
|
{/* Cancel Button
|
||||||
|
Returns user to dashboard without saving
|
||||||
|
Immediate navigation with no confirmation */}
|
||||||
<button
|
<button
|
||||||
className="button cancel"
|
className="button cancel"
|
||||||
onClick={() => navigate('/dashboard')}
|
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
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{/* Draft Save Button
|
||||||
|
Saves current state as draft
|
||||||
|
Disabled during save operations */}
|
||||||
<button
|
<button
|
||||||
className="button draft"
|
className="button draft"
|
||||||
onClick={() => handleSave('draft')}
|
onClick={() => handleSave('draft')}
|
||||||
disabled={saving}
|
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
|
Save as Draft
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{/* Publish/Update Button
|
||||||
|
Context-aware label based on current status
|
||||||
|
Handles both initial publish and updates */}
|
||||||
<button
|
<button
|
||||||
className="button publish"
|
className="button publish"
|
||||||
onClick={() => handleSave('published')}
|
onClick={() => handleSave('published')}
|
||||||
disabled={saving}
|
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'}
|
{status === 'published' ? 'Update' : 'Publish'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</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' && (
|
{status === 'published' && (
|
||||||
<div className="publish-settings">
|
<div className="publish-settings">
|
||||||
|
{/* DateTime Input Field
|
||||||
|
Allows scheduling of publication
|
||||||
|
Uses HTML5 datetime-local input */}
|
||||||
<label>
|
<label>
|
||||||
Publication Date:
|
Publication Date:
|
||||||
<input
|
<input
|
||||||
type="datetime-local"
|
type="datetime-local"
|
||||||
value={publishDate}
|
value={publishDate}
|
||||||
onChange={(e) => setPublishDate(e.target.value)}
|
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>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Tags Management Section
|
||||||
|
Handles both tag display and input
|
||||||
|
Implements add/remove tag functionality */}
|
||||||
<div className="tags-input">
|
<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">
|
<div className="tags-list">
|
||||||
{tags.map((tag, index) => (
|
{tags.map((tag, index) => (
|
||||||
<span key={index} className="tag">
|
<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}
|
{tag}
|
||||||
<button
|
<button
|
||||||
onClick={() => handleRemoveTag(tag)}
|
onClick={() => handleRemoveTag(tag)}
|
||||||
className="remove-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>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Tag Input Field
|
||||||
|
Handles new tag entry
|
||||||
|
Implements enter-to-add functionality */}
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Add tags (press Enter)"
|
placeholder="Add tags (press Enter)"
|
||||||
value={tagInput}
|
value={tagInput}
|
||||||
onChange={(e) => setTagInput(e.target.value)}
|
onChange={(e) => setTagInput(e.target.value)}
|
||||||
onKeyDown={handleAddTag}
|
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>
|
</div>
|
||||||
|
|
||||||
|
{/* Markdown Editor Section
|
||||||
|
Main content editing area
|
||||||
|
Implements drag-and-drop image upload */}
|
||||||
<div
|
<div
|
||||||
className="editor-container"
|
className="editor-container"
|
||||||
onDrop={handleDrop}
|
onDrop={handleDrop}
|
||||||
onDragOver={(e) => e.preventDefault()}
|
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
|
<MDEditor
|
||||||
value={content}
|
value={content}
|
||||||
onChange={setContent}
|
onChange={setContent}
|
||||||
@@ -209,6 +643,14 @@ export default function Editor() {
|
|||||||
textareaProps={{
|
textareaProps={{
|
||||||
placeholder: 'Write your post in Markdown...'
|
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>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user