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:
2025-09-28 10:44:44 -05:00
parent 77c385dc9b
commit 918121cd66
7 changed files with 339 additions and 0 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 MiB

View 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;

View 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;

View 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;

View 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;

View 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
View 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);
};