101 lines
4.0 KiB
TypeScript
101 lines
4.0 KiB
TypeScript
import React, { useState } from 'react';
|
|
import { LayoutPanelLeft, LayoutPanelRight, LayoutTemplate } from 'lucide-react';
|
|
|
|
interface MarkdownEditorProps {
|
|
value: string;
|
|
onChange: (val: string) => void;
|
|
placeholder?: string;
|
|
}
|
|
|
|
type ViewMode = 'edit' | 'preview' | 'split';
|
|
|
|
export default function MarkdownEditor({ value, onChange, placeholder }: MarkdownEditorProps) {
|
|
const [mode, setMode] = useState<ViewMode>('split');
|
|
|
|
const parseMarkdown = (text: string) => {
|
|
if (!text) return <p className="text-muted-foreground italic">No content...</p>;
|
|
|
|
return text.split('\n\n').map((paragraph, idx) => {
|
|
let content = paragraph;
|
|
|
|
// Headers
|
|
if (content.startsWith('## ')) {
|
|
return <h2 key={idx} className="text-xl font-bold mt-4 mb-2">{content.replace('## ', '')}</h2>;
|
|
}
|
|
if (content.startsWith('# ')) {
|
|
return <h1 key={idx} className="text-2xl font-bold mt-6 mb-3">{content.replace('# ', '')}</h1>;
|
|
}
|
|
|
|
// Lists
|
|
if (content.startsWith('- ')) {
|
|
const items = paragraph.split('\n').filter(line => line.startsWith('- '));
|
|
return (
|
|
<ul key={idx} className="list-disc pl-5 mb-4 space-y-1">
|
|
{items.map((item, i) => (
|
|
<li key={i} dangerouslySetInnerHTML={{ __html: applyInlineFormatting(item.replace('- ', '')) }} />
|
|
))}
|
|
</ul>
|
|
);
|
|
}
|
|
|
|
return <p key={idx} className="mb-4" dangerouslySetInnerHTML={{ __html: applyInlineFormatting(content) }} />;
|
|
});
|
|
};
|
|
|
|
const applyInlineFormatting = (text: string) => {
|
|
return text
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
|
|
.replace(/\*(.*?)\*/g, '<em>$1</em>')
|
|
.replace(/`(.*?)`/g, '<code class="bg-slate-100 dark:bg-slate-800 px-1 rounded font-mono text-sm">$1</code>');
|
|
};
|
|
|
|
return (
|
|
<div className="flex flex-col h-full border rounded-lg overflow-hidden bg-white dark:bg-slate-900 border-slate-200 dark:border-slate-800">
|
|
<div className="flex items-center justify-between px-3 py-2 border-b bg-slate-50 dark:bg-slate-950 border-slate-200 dark:border-slate-800">
|
|
<span className="text-xs font-medium text-slate-500 uppercase tracking-wider">Markdown Editor</span>
|
|
<div className="flex gap-1">
|
|
<button
|
|
onClick={() => setMode('edit')}
|
|
className={`p-1.5 rounded ${mode === 'edit' ? 'bg-slate-200 dark:bg-slate-800 text-slate-900 dark:text-slate-100' : 'text-slate-500 hover:bg-slate-100 dark:hover:bg-slate-800'}`}
|
|
title="Edit Mode"
|
|
>
|
|
<LayoutPanelLeft size={16} />
|
|
</button>
|
|
<button
|
|
onClick={() => setMode('split')}
|
|
className={`p-1.5 rounded ${mode === 'split' ? 'bg-slate-200 dark:bg-slate-800 text-slate-900 dark:text-slate-100' : 'text-slate-500 hover:bg-slate-100 dark:hover:bg-slate-800'}`}
|
|
title="Split Mode"
|
|
>
|
|
<LayoutTemplate size={16} />
|
|
</button>
|
|
<button
|
|
onClick={() => setMode('preview')}
|
|
className={`p-1.5 rounded ${mode === 'preview' ? 'bg-slate-200 dark:bg-slate-800 text-slate-900 dark:text-slate-100' : 'text-slate-500 hover:bg-slate-100 dark:hover:bg-slate-800'}`}
|
|
title="Preview Mode"
|
|
>
|
|
<LayoutPanelRight size={16} />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex flex-1 overflow-hidden">
|
|
{(mode === 'edit' || mode === 'split') && (
|
|
<textarea
|
|
className="flex-1 p-4 resize-none outline-none font-mono text-sm bg-transparent border-r border-slate-200 dark:border-slate-800"
|
|
value={value}
|
|
onChange={(e) => onChange(e.target.value)}
|
|
placeholder={placeholder}
|
|
/>
|
|
)}
|
|
{(mode === 'preview' || mode === 'split') && (
|
|
<div className="flex-1 p-4 overflow-y-auto prose prose-sm max-w-none dark:prose-invert">
|
|
{parseMarkdown(value)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
} |