All files / apps/web/src/frontoffice/components MarkdownViewerModal.jsx

0% Statements 0/73
0% Branches 0/44
0% Functions 0/14
0% Lines 0/62

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 ![alt](url))
        .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(/^&gt; (.*$)/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