feat: Modularized Editor into Wysiwyg and Markdown/Viewer components
- Add MetedataEditor for meta inputs - Add toggle between editor modes - Add Breadcrumb for clearer navigation - Add custom tool for formatting inline code in WysiwygEditor
This commit is contained in:
Binary file not shown.
Before Width: | Height: | Size: 2.0 MiB |
68
src/components/Editor/MarkdownEditor.jsx
Normal file
68
src/components/Editor/MarkdownEditor.jsx
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
// MarkdownEditor.jsx
|
||||||
|
// KendoReact Splitter implementation for dual-pane markdown editing with live preview
|
||||||
|
// Demonstrates advanced layout components and real-time markdown processing
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Splitter, SplitterPane } from '@progress/kendo-react-layout';
|
||||||
|
import { marked } from 'marked';
|
||||||
|
|
||||||
|
function MarkdownEditor({ markdownContent, onMarkdownChange, onSplitterChange, panes }) {
|
||||||
|
return (
|
||||||
|
// KendoReact Splitter for dual-pane markdown editing with live preview
|
||||||
|
<Splitter
|
||||||
|
style={{ height: '575px', width: '100%' }}
|
||||||
|
panes={panes}
|
||||||
|
onChange={onSplitterChange}
|
||||||
|
>
|
||||||
|
<SplitterPane>
|
||||||
|
<div style={{ padding: '10px' }}>
|
||||||
|
<h4 style={{ margin: '0 0 10px 0' }}>Markdown Editor</h4>
|
||||||
|
<textarea
|
||||||
|
value={markdownContent}
|
||||||
|
onChange={onMarkdownChange}
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
height: '500px',
|
||||||
|
fontFamily: 'Arial, sans-serif',
|
||||||
|
fontSize: '14px',
|
||||||
|
lineHeight: '1.5',
|
||||||
|
backgroundColor: '#f5f1e9',
|
||||||
|
color: '#3d3d3d',
|
||||||
|
resize: 'none',
|
||||||
|
border: '1px solid #e0e0e0',
|
||||||
|
borderRadius: '8px',
|
||||||
|
padding: '10px',
|
||||||
|
outline: 'none',
|
||||||
|
marginBottom: '10px'
|
||||||
|
}}
|
||||||
|
placeholder="Enter markdown content..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</SplitterPane>
|
||||||
|
<SplitterPane>
|
||||||
|
<div style={{ padding: '10px' }}>
|
||||||
|
<h4 style={{ margin: '0 0 10px 0' }}>Live Preview</h4>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
height: '500px',
|
||||||
|
overflow: 'auto',
|
||||||
|
border: '1px solid #e0e0e0',
|
||||||
|
borderRadius: '8px',
|
||||||
|
padding: '10px',
|
||||||
|
fontFamily: 'Arial, sans-serif',
|
||||||
|
fontSize: '14px',
|
||||||
|
lineHeight: '1.5',
|
||||||
|
backgroundColor: '#f5f1e9',
|
||||||
|
color: '#3d3d3d',
|
||||||
|
marginBottom: '10px'
|
||||||
|
}}
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: marked(markdownContent)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</SplitterPane>
|
||||||
|
</Splitter>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MarkdownEditor;
|
71
src/components/Editor/MetadataEditor.jsx
Normal file
71
src/components/Editor/MetadataEditor.jsx
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
// MetadataEditor.jsx
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Input } from '@progress/kendo-react-inputs';
|
||||||
|
import { Label } from '@progress/kendo-react-labels';
|
||||||
|
import { Button } from '@progress/kendo-react-buttons';
|
||||||
|
|
||||||
|
function MetadataEditor({ postData }) {
|
||||||
|
const [title, setTitle] = useState(postData?.title || '');
|
||||||
|
const [tags, setTags] = useState(postData?.tags?.join(', ') || '');
|
||||||
|
const [headerImage, setHeaderImage] = useState(postData?.header?.image || '');
|
||||||
|
|
||||||
|
const handleTitleChange = (e) => {
|
||||||
|
setTitle(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTagsChange = (e) => {
|
||||||
|
setTags(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleHeaderImageChange = (e) => {
|
||||||
|
const file = e.target.files[0];
|
||||||
|
if (file) {
|
||||||
|
console.log('Header image uploaded:', file);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ padding: '20px', borderBottom: '1px solid #e0e0e0' }}>
|
||||||
|
<div style={{ display: 'flex', gap: '20px', marginBottom: '15px', alignItems: 'center' }}>
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept=".webp,.png,.jpg,.jpeg,.avif,.svg"
|
||||||
|
onChange={handleHeaderImageChange}
|
||||||
|
style={{ display: 'none' }}
|
||||||
|
id="header-image-upload"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
onClick={() => document.getElementById('header-image-upload').click()}
|
||||||
|
>
|
||||||
|
Upload Header Image
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ flex: 1, display: 'flex', alignItems: 'center', gap: '10px' }}>
|
||||||
|
<Label style={{ minWidth: '60px' }}>Title</Label>
|
||||||
|
<Input
|
||||||
|
value={title}
|
||||||
|
onChange={handleTitleChange}
|
||||||
|
placeholder="Enter post title"
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ flex: 1, display: 'flex', alignItems: 'center', gap: '10px' }}>
|
||||||
|
<Label style={{ minWidth: '60px' }}>Tags</Label>
|
||||||
|
<Input
|
||||||
|
value={tags}
|
||||||
|
onChange={handleTagsChange}
|
||||||
|
placeholder="Enter tags (comma separated)"
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MetadataEditor;
|
26
src/components/Editor/custom/EditorModeToggle.jsx
Normal file
26
src/components/Editor/custom/EditorModeToggle.jsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
// EditorModeToggle.jsx
|
||||||
|
// Custom KendoReact component for switching between HTML and Markdown editing modes
|
||||||
|
// Demonstrates reusable UI components and state management patterns
|
||||||
|
import React from 'react';
|
||||||
|
import { Button } from '@progress/kendo-react-buttons';
|
||||||
|
|
||||||
|
function EditorModeToggle({ editMode, onToggle }) {
|
||||||
|
return (
|
||||||
|
// Toggle button positioned above the editor with consistent styling
|
||||||
|
<div style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
marginBottom: '8px'
|
||||||
|
}}>
|
||||||
|
<Button
|
||||||
|
onClick={onToggle}
|
||||||
|
size="small"
|
||||||
|
icon={editMode === 'html' ? 'code' : 'edit'}
|
||||||
|
>
|
||||||
|
{editMode === 'html' ? 'Switch to Markdown' : 'Switch to HTML'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EditorModeToggle;
|
49
src/components/Editor/custom/InlineCodeTool.jsx
Normal file
49
src/components/Editor/custom/InlineCodeTool.jsx
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
// InlineCodeTool.jsx
|
||||||
|
// Custom KendoReact Editor tool for inline code formatting
|
||||||
|
import React from 'react';
|
||||||
|
import { Button } from '@progress/kendo-react-buttons';
|
||||||
|
import { SvgIcon } from '@progress/kendo-react-common';
|
||||||
|
import { codeSnippetIcon } from '@progress/kendo-svg-icons';
|
||||||
|
|
||||||
|
const InlineCodeTool = (props) => {
|
||||||
|
const { view } = props;
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
if (view) {
|
||||||
|
// Access ProseMirror editor state and dispatch
|
||||||
|
const { state, dispatch } = view;
|
||||||
|
const markType = state.schema.marks.code;
|
||||||
|
const { from, to } = state.selection;
|
||||||
|
|
||||||
|
if (markType) {
|
||||||
|
const tr = state.tr;
|
||||||
|
// Check if selected text already has code mark
|
||||||
|
const hasMark = state.doc.rangeHasMark(from, to, markType);
|
||||||
|
|
||||||
|
if (hasMark) {
|
||||||
|
// Remove code mark if already present
|
||||||
|
tr.removeMark(from, to, markType);
|
||||||
|
} else {
|
||||||
|
// Add code mark to selected text
|
||||||
|
tr.addMark(from, to, markType.create());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dispatch transaction to apply changes
|
||||||
|
dispatch(tr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
// KendoReact Button with SVG icon
|
||||||
|
<Button
|
||||||
|
onClick={handleClick}
|
||||||
|
title="Inline Code"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
<SvgIcon icon={codeSnippetIcon} />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default InlineCodeTool;
|
75
src/components/UI/Breadcrumb.jsx
Normal file
75
src/components/UI/Breadcrumb.jsx
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
// Breadcrumb.jsx
|
||||||
|
import React from 'react';
|
||||||
|
import { Breadcrumb } from '@progress/kendo-react-layout';
|
||||||
|
import { useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
|
const CampfireBreadcrumb = () => {
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
const getBreadcrumbs = () => {
|
||||||
|
const path = location.pathname;
|
||||||
|
const pathSegments = path.split('/').filter(segment => segment);
|
||||||
|
|
||||||
|
if (path === '/dashboard' || path === '/') {
|
||||||
|
return [{ id: 'dashboard', text: 'Dashboard' }];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path === '/posts') {
|
||||||
|
return [
|
||||||
|
{ id: 'dashboard', text: 'Dashboard' },
|
||||||
|
{ id: 'posts', text: 'Published Posts' }
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path === '/drafts') {
|
||||||
|
return [
|
||||||
|
{ id: 'dashboard', text: 'Dashboard' },
|
||||||
|
{ id: 'drafts', text: 'Drafts' }
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path === '/editor') {
|
||||||
|
return [
|
||||||
|
{ id: 'editor', text: 'Editor' },
|
||||||
|
{ id: 'new-post', text: 'New Post' }
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path.startsWith('/editor/')) {
|
||||||
|
const slug = pathSegments[1];
|
||||||
|
return [
|
||||||
|
{ id: 'editor', text: 'Editor' },
|
||||||
|
{ id: 'edit-post', text: 'Edit Post' } // Placeholder for actual title
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [{ id: 'dashboard', text: 'Dashboard' }];
|
||||||
|
};
|
||||||
|
|
||||||
|
const breadcrumbData = getBreadcrumbs();
|
||||||
|
console.log('Breadcrumb data:', breadcrumbData);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
marginLeft: '120px',
|
||||||
|
color: '#d94f27',
|
||||||
|
fontSize: '14px',
|
||||||
|
lineHeight: '1.2',
|
||||||
|
backgroundColor: 'transparent'
|
||||||
|
}}>
|
||||||
|
<style>
|
||||||
|
{`
|
||||||
|
.k-breadcrumb {
|
||||||
|
background-color: transparent !important;
|
||||||
|
}
|
||||||
|
.k-breadcrumb .k-breadcrumb-item {
|
||||||
|
background-color: transparent !important;
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
</style>
|
||||||
|
<Breadcrumb data={breadcrumbData} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CampfireBreadcrumb;
|
50
src/data/postsCache.js
Normal file
50
src/data/postsCache.js
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
// postsCache.js
|
||||||
|
// Caching layer for blog post data
|
||||||
|
// Prevents multiple simultaneous API calls and provides efficient data access
|
||||||
|
import { loadPosts } from './blog-post-data';
|
||||||
|
|
||||||
|
// Cache state management
|
||||||
|
let postsCache = null;
|
||||||
|
let isLoading = false;
|
||||||
|
let loadPromise = null;
|
||||||
|
|
||||||
|
// Singleton pattern with promise deduplication for efficient data loading
|
||||||
|
export const getPosts = async () => {
|
||||||
|
// Return cached data immediately if available
|
||||||
|
if (postsCache) {
|
||||||
|
return postsCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent duplicate loading - wait for existing promise
|
||||||
|
if (isLoading && loadPromise) {
|
||||||
|
return loadPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start loading process with promise caching
|
||||||
|
isLoading = true;
|
||||||
|
loadPromise = loadPosts().then(posts => {
|
||||||
|
postsCache = posts;
|
||||||
|
isLoading = false;
|
||||||
|
return posts;
|
||||||
|
});
|
||||||
|
|
||||||
|
return loadPromise;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Filter published posts for dashboard display
|
||||||
|
export const getPublishedPosts = async () => {
|
||||||
|
const posts = await getPosts();
|
||||||
|
return posts.filter(post => post.published);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Filter draft posts for editor management
|
||||||
|
export const getDraftPosts = async () => {
|
||||||
|
const posts = await getPosts();
|
||||||
|
return posts.filter(post => !post.published);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Find specific post by slug for editor functionality
|
||||||
|
export const getPostBySlug = async (slug) => {
|
||||||
|
const posts = await getPosts();
|
||||||
|
return posts.find(post => post.slug === slug);
|
||||||
|
};
|
Reference in New Issue
Block a user