A comprehensive markdown editor for creating and editing articles with real-time preview, auto-save, and rich formatting tools.
src/components/Editor/ArticleEditor.tsx
ArticleEditor is a full-featured article editing interface that provides markdown editing capabilities, real-time preview, auto-save functionality, and article management features.
interface ArticleEditorProps {
uuid?: string;
article?: Article;
}uuid: Unique identifier for existing article (for editing mode)article: Article data object (for editing existing articles)
import ArticleEditor from '@/components/Editor/ArticleEditor';
function NewArticlePage() {
return (
<div className="container mx-auto">
<ArticleEditor />
</div>
);
}function EditArticlePage({ articleId }: { articleId: string }) {
const { data: article } = useQuery({
queryKey: ['article', articleId],
queryFn: () => fetchArticle(articleId)
});
if (!article) return <div>Loading...</div>;
return (
<ArticleEditor
uuid={article.id}
article={article}
/>
);
}- Auto-save: Automatically saves changes after 1 second of inactivity
- Debounced updates: Prevents excessive API calls during typing
- Live status: Shows saving status and last saved time
- Form validation: Uses Zod schema validation
- Write mode: Markdown editing with toolbar
- Preview mode: Rendered markdown preview
- Toggle switch: Easy switching between modes
- Heading: Insert H2 headings
- Bold: Bold text formatting
- Italic: Italic text formatting
- Image: Insert image markdown
- Publish/Unpublish: Toggle article publication status
- Draft indication: Visual status indicators
- Settings drawer: Additional article configuration
- Navigation: Back to dashboard
const editorForm = useForm({
defaultValues: {
title: article?.title || "",
body: article?.body || "",
},
resolver: zodResolver(ArticleRepositoryInput.updateArticleInput),
});// Title auto-save (1 second delay)
const setDebouncedTitle = useDebouncedCallback(
handleDebouncedSaveTitle,
1000
);
// Body auto-save (1 second delay)
const setDebouncedBody = useDebouncedCallback(
handleDebouncedSaveBody,
1000
);const editor = useMarkdownEditor({
ref: bodyRef,
onChange: handleBodyContentChange,
});useTranslation: InternationalizationuseToggle: Modal and drawer state managementuseAutosizeTextArea: Auto-resizing title inputuseDebouncedCallback: Auto-save functionalityuseMarkdownEditor: Markdown formatting commandsuseAppConfirm: Confirmation dialogs
useMutation: Article creation and updates- Optimistic updates and error handling
- Triggers after 1 second of no typing
- Creates new article if none exists
- Updates existing article title
- Triggers after 1 second of no typing
- Creates article with default title if new
- Updates existing article content
{updateMyArticleMutation.isPending ? (
<p>{_t("Saving")}...</p>
) : (
article?.updated_at && (
<p>
({_t("Saved")} {formattedTime(article.updated_at, lang)})
</p>
)
)}// Heading command
editor?.executeCommand("heading")
// Bold formatting
editor?.executeCommand("bold")
// Italic formatting
editor?.executeCommand("italic")
// Image insertion
editor?.executeCommand("image")const renderEditorToolbar = () => (
<div className="flex w-full gap-6 p-2 my-2 bg-muted">
<EditorCommandButton
onClick={() => editor?.executeCommand("heading")}
Icon={<HeadingIcon />}
/>
<EditorCommandButton
onClick={() => editor?.executeCommand("bold")}
Icon={<FontBoldIcon />}
/>
{/* ... more buttons */}
</div>
);{editorMode === "write" ? (
<textarea
value={watchedBody}
onChange={handleBodyContentChange}
// ... props
/>
) : (
<div className="content-typography">
{markdocParser(watchedBody ?? "")}
</div>
)}const handlePublishToggle = useCallback(() => {
appConfig.show({
title: _t("Are you sure?"),
onConfirm: () => {
updateMyArticleMutation.mutate({
article_id: uuid,
is_published: !article?.is_published,
});
},
});
}, [/* dependencies */]);<p className={clsx("px-2 py-1 text-foreground", {
"bg-green-100": article?.is_published,
"bg-red-100": !article?.is_published,
})}>
{article?.is_published ? (
<span className="text-success">{_t("Published")}</span>
) : (
<span className="text-destructive">{_t("Draft")}</span>
)}
</p>- Hidden preview/publish buttons on mobile
- Responsive toolbar layout
- Touch-friendly interface
- Adaptive spacing and sizing
- Full toolbar visibility
- Preview mode toggle
- Publish/unpublish controls
- Settings access
- Title and body changes debounced to 1 second
- Prevents excessive API calls
- Maintains smooth editing experience
- Event handlers memoized with useCallback
- Dependency arrays optimized
- Prevents unnecessary re-renders
- React Hook Form for efficient form management
- Minimal re-renders on value changes
- Proper validation integration
onError: (err) => {
console.error("Error creating article:", err);
alert(
err instanceof Error
? err.message
: "Failed to create article. Please try again."
);
}- Confirmation dialogs for destructive actions
- Auto-save prevents data loss
- Proper error state management
The component supports multiple languages:
- All UI text is translatable
- Date formatting respects locale
- Error messages are localized
- Placeholder text is translated
- ArticleEditorDrawer: Article settings and metadata
- EditorCommandButton: Toolbar button component
- useMarkdownEditor: Markdown editing functionality
- markdocParser: Markdown to HTML conversion
- React Hook Form: Form state management
- Zod: Input validation
- TanStack Query: Server state management
// Always handle loading states
if (isLoading) return <LoadingSpinner />;
// Proper error boundaries
if (error) return <ErrorMessage error={error} />;
// Optimistic updates for better UX
onMutate: (variables) => {
// Optimistically update UI
}// Prevent data loss with auto-save
const setDebouncedBody = useDebouncedCallback(saveContent, 1000);
// Provide visual feedback
{isSaving && <p>Saving...</p>}
// Confirm destructive actions
appConfirm.show({
title: "Are you sure?",
onConfirm: performAction
});function CreateBlogPost() {
return (
<PageLayout>
<ArticleEditor />
</PageLayout>
);
}function EditWorkflow({ articleId }: { articleId: string }) {
const { data: article, isLoading } = useArticle(articleId);
if (isLoading) return <Skeleton />;
return (
<ArticleEditor
uuid={articleId}
article={article}
/>
);
}function DraftEditor({ draftId }: { draftId: string }) {
const { data: draft } = useDraft(draftId);
return (
<ArticleEditor
uuid={draftId}
article={draft}
/>
);
}