literate programming in the editor.jsx
This commit is contained in:
@@ -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:
|
||||
// <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
|
||||
|
||||
/**
|
||||
* 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<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
|
||||
|
||||
// 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<string>
|
||||
};
|
||||
|
||||
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
|
||||
<input
|
||||
*/}
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Post title"
|
||||
value={title}
|
||||
|
||||
Reference in New Issue
Block a user