EmberClone/apps/web/src/components/MarkdownEditor.tsx

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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.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>
);
}