import React, { useState, useEffect } from 'react'; import { Modal } from './Modal'; import './MarkdownViewer.scss'; interface UrlMapping { localPath: string; publicUrl: string; } interface MarkdownViewerProps { filePaths: string[]; urlMappings?: UrlMapping[]; } export const MarkdownViewer: React.FC = ({ filePaths, urlMappings = [] }) => { const [contents, setContents] = useState>({}); const [loading, setLoading] = useState>({}); const [errors, setErrors] = useState>({}); const [expandedFile, setExpandedFile] = useState(null); const [ReactMarkdown, setReactMarkdown] = useState(null); const [isClient, setIsClient] = useState(false); // Check if we're on the client side and load ReactMarkdown useEffect(() => { setIsClient(true); import('react-markdown').then((module) => { setReactMarkdown(() => module.default); }); }, []); useEffect(() => { filePaths.forEach(async (filePath) => { if (contents[filePath] || loading[filePath]) return; setLoading(prev => ({ ...prev, [filePath]: true })); try { // Apply URL mappings let fetchUrl = filePath; for (const mapping of urlMappings) { if (filePath.startsWith(mapping.localPath)) { fetchUrl = filePath.replace(mapping.localPath, mapping.publicUrl); break; } } const response = await fetch(fetchUrl, { mode: 'cors', headers: { 'Accept': 'text/plain, text/markdown, */*' } }); if (!response.ok) { throw new Error(`Failed to load ${filePath}: ${response.status} ${response.statusText}`); } const text = await response.text(); // Check if we got HTML instead of markdown (common in dev environments) if (text.includes('') || text.includes(' ({ ...prev, [filePath]: text })); } catch (error) { let errorMessage = 'Failed to load file'; if (error instanceof Error) { errorMessage = error.message; // Add specific message for CORS errors if (error.message.includes('Failed to fetch') || error.name === 'TypeError') { errorMessage = `CORS error: Unable to load ${filePath}. The server needs to allow cross-origin requests.`; } } setErrors(prev => ({ ...prev, [filePath]: errorMessage })); } finally { setLoading(prev => ({ ...prev, [filePath]: false })); } }); }, [filePaths, urlMappings]); if (filePaths.length === 0) return null; return ( <>
{filePaths.map((filePath) => { const fileName = filePath.split('/').pop() || filePath; return (
{fileName}
{loading[filePath] &&
Loading...
} {errors[filePath] &&
{errors[filePath]}
} {contents[filePath] && ReactMarkdown && isClient && ( {contents[filePath]} )}
); })}
{expandedFile && contents[expandedFile] && ( setExpandedFile(null)} contentClassName="markdown-modal" >

{expandedFile.split('/').pop() || expandedFile}

{ReactMarkdown && isClient && ( {contents[expandedFile]} )}
)} ); };