Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 | import { useState, useEffect } from 'react'
import { FileText, Copy } from 'lucide-react'
import toast from 'react-hot-toast'
// Modal component for viewing Markdown content with proper rendering
function MarkdownViewerModal({ isOpen, onClose, content, title }) {
const [renderedContent, setRenderedContent] = useState('')
// Close on escape key - must be before any conditional return
useEffect(() => {
if (!isOpen) return
const handleEscape = (e) => {
if (e.key === 'Escape') onClose()
}
window.addEventListener('keydown', handleEscape)
return () => window.removeEventListener('keydown', handleEscape)
}, [isOpen, onClose])
// Render markdown when content changes
useEffect(() => {
if (!content || !isOpen) return
// Function to render tables
const renderTables = (text) => {
const lines = text.split('\n')
let result = []
let inTable = false
let tableLines = []
let isHeader = false
for (let i = 0; i < lines.length; i++) {
const line = lines[i]
// Check if line is a table row
if (line.trim().startsWith('|') && line.trim().endsWith('|')) {
if (!inTable) {
inTable = true
tableLines = []
isHeader = true
}
tableLines.push(line)
} else if (inTable && line.trim().match(/^\|[-:\s|]+\|$/)) {
// Separator line |---|---|
tableLines.push(line)
isHeader = false
} else if (inTable) {
// End of table
if (tableLines.length > 0) {
result.push(renderTableHtml(tableLines, isHeader))
}
inTable = false
tableLines = []
result.push(line)
} else {
result.push(line)
}
}
// Don't forget last table
if (inTable && tableLines.length > 0) {
result.push(renderTableHtml(tableLines, isHeader))
}
return result.join('\n')
}
const renderTableHtml = (lines, hasHeader) => {
let html = '<div class="overflow-x-auto my-4"><table class="min-w-full border-collapse border border-gray-300">'
lines.forEach((line, index) => {
// Skip separator lines
if (line.trim().match(/^\|[-:\s|]+\|$/)) return
const cells = line.split('|').filter(c => c.trim() !== '').map(c => c.trim())
if (index === 0 && hasHeader) {
// Header row
html += '<thead class="bg-gray-100"><tr>'
cells.forEach(cell => {
html += `<th class="border border-gray-300 px-4 py-2 text-left font-semibold text-gray-700">${cell}</th>`
})
html += '</tr></thead><tbody>'
} else {
// Data row
if (index === 1 && hasHeader) html = html.replace('</thead>', '</thead><tbody>')
html += '<tr class="hover:bg-gray-50">'
cells.forEach(cell => {
html += `<td class="border border-gray-300 px-4 py-2 text-gray-600">${cell}</td>`
})
html += '</tr>'
}
})
html += '</tbody></table></div>'
return html
}
// Simple but effective markdown to HTML conversion
const renderMarkdown = (md) => {
if (!md) return ''
// First render tables
let html = renderTables(md)
return html
// Headers (before other inline elements)
.replace(/^#### (.*$)/gim, '<h4 class="text-lg font-semibold mt-4 mb-2 text-gray-800">$1</h4>')
.replace(/^### (.*$)/gim, '<h3 class="text-xl font-semibold mt-5 mb-3 text-gray-800">$1</h3>')
.replace(/^## (.*$)/gim, '<h2 class="text-2xl font-bold mt-6 mb-4 text-gray-900 border-b pb-2">$1</h2>')
.replace(/^# (.*$)/gim, '<h1 class="text-3xl font-bold mt-8 mb-4 text-gray-900">$1</h1>')
// Images (MUST be before links to avoid conflicts with )
.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<img src="$2" alt="$1" class="max-w-full h-auto rounded-lg shadow-md my-4" loading="lazy" />')
// Links (after images)
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer" class="text-blue-600 hover:text-blue-800 underline">$1</a>')
// Bold and italic
.replace(/\*\*\*(.*?)\*\*\*/g, '<strong><em>$1</em></strong>')
.replace(/\*\*(.*?)\*\*/g, '<strong class="font-semibold">$1</strong>')
.replace(/\*(.*?)\*/g, '<em class="italic">$1</em>')
// Code blocks
.replace(/```(\w+)?\n([\s\S]*?)```/g, '<pre class="bg-gray-900 text-gray-100 p-4 rounded-lg overflow-x-auto my-4 text-sm"><code>$2</code></pre>')
// Inline code
.replace(/`([^`]+)`/g, '<code class="bg-gray-100 text-red-600 px-2 py-1 rounded text-sm font-mono">$1</code>')
// Blockquotes
.replace(/^> (.*$)/gim, '<blockquote class="border-l-4 border-blue-500 pl-4 py-2 my-4 bg-blue-50 italic text-gray-700">$1</blockquote>')
// Lists
.replace(/^(\s*)[-*+] (.*$)/gim, '<li class="ml-4 mb-1 list-disc">$2</li>')
.replace(/^(\s*)\d+\. (.*$)/gim, '<li class="ml-4 mb-1 list-decimal">$2</li>')
// Horizontal rule
.replace(/^---+$/gim, '<hr class="my-6 border-t-2 border-gray-200" />')
// Line breaks
.replace(/\n\n/g, '</p><p class="mb-4 leading-relaxed">')
.replace(/\n/g, '<br/>')
// Wrap in paragraphs (but not if already wrapped in HTML tags)
.replace(/^([^<].*[^>])$/gim, '<p class="mb-4 leading-relaxed">$1</p>')
}
setRenderedContent(renderMarkdown(content))
}, [content, isOpen])
// Return null after all hooks are called
if (!isOpen || !content) return null
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm p-4">
<div className="bg-white rounded-xl shadow-2xl max-w-4xl w-full max-h-[90vh] overflow-hidden flex flex-col">
{/* Header */}
<div className="flex items-center justify-between p-4 border-b bg-gray-50">
<div className="flex items-center gap-3">
<FileText className="w-6 h-6 text-blue-600" />
<div>
<h3 className="text-lg font-semibold text-gray-900">{title || 'Document'}</h3>
<p className="text-sm text-gray-500">Aperçu Markdown</p>
</div>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => {
navigator.clipboard.writeText(content)
toast.success('Markdown copié !')
}}
className="px-3 py-2 text-sm text-blue-600 hover:bg-blue-50 rounded-lg transition-colors flex items-center gap-1"
>
<Copy className="w-4 h-4" />
Copier MD
</button>
<button
onClick={onClose}
className="p-2 hover:bg-gray-200 rounded-full transition-colors"
>
<svg className="w-6 h-6 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
{/* Content */}
<div className="flex-1 overflow-auto p-8 bg-white">
<div
className="prose prose-lg max-w-none"
dangerouslySetInnerHTML={{ __html: renderedContent }}
/>
</div>
{/* Footer */}
<div className="p-3 bg-gray-50 border-t text-center text-sm text-gray-500">
Appuyez sur Echap pour fermer
</div>
</div>
</div>
)
}
export default MarkdownViewerModal |