File size: 8,840 Bytes
69a3dd3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
196
197
198
199
200
201
202
203
import { useEffect, useState, useMemo } from 'react';
import MarkdownRenderer from './MarkdownRenderer';
import { X, Copy, Check, Download, PanelRightClose, PanelRightOpen } from 'lucide-react';
import 'highlight.js/styles/github-dark.css';

interface FilePreviewModalProps {
  isOpen: boolean;
  onClose: () => void;
  content: string;
}

// Helper to generate IDs from text
const slugify = (text: string) => {
  return text
    .toString()
    .toLowerCase()
    .trim()
    .replace(/\s+/g, '-')
    .replace(/[^\w\u4e00-\u9fa5-]+/g, '')
    .replace(/-+/g, '-');
};

export default function FilePreviewModal({ isOpen, onClose, content }: FilePreviewModalProps) {
  const [copied, setCopied] = useState(false);
  const [showToc, setShowToc] = useState(false);

  useEffect(() => {
    if (isOpen) {
      document.body.style.overflow = 'hidden';
    } else {
      document.body.style.overflow = 'unset';
    }
    return () => {
      document.body.style.overflow = 'unset';
    };
  }, [isOpen]);

  // Generate TOC data
  const toc = useMemo(() => {
    const lines = content.split('\n');
    const headers: { level: number; text: string; id: string }[] = [];
    
    // Simple regex to match headers. Note: this won't handle code blocks correctly if they contain #
    // But for a simple TOC it's usually "good enough". 
    // A more robust way would be to traverse the AST, but that requires more setup.
    let inCodeBlock = false;
    
    lines.forEach(line => {
      if (line.trim().startsWith('```')) {
        inCodeBlock = !inCodeBlock;
        return;
      }
      if (inCodeBlock) return;

      const match = line.match(/^(#{1,3})\s+(.+)$/);
      if (match) {
        const level = match[1].length;
        const text = match[2].trim();
        const id = slugify(text);
        headers.push({ level, text, id });
      }
    });
    
    return headers;
  }, [content]);

  if (!isOpen) return null;

  const handleCopy = () => {
    navigator.clipboard.writeText(content);
    setCopied(true);
    setTimeout(() => setCopied(false), 2000);
  };

  const handleDownload = () => {
    const blob = new Blob([content], { type: 'text/markdown' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = 'content.md';
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
    URL.revokeObjectURL(url);
  };

  const scrollToHeader = (id: string) => {
    const element = document.getElementById(id);
    if (element) {
      element.scrollIntoView({ behavior: 'smooth' });
    }
  };

  // Custom components for ReactMarkdown to add IDs - Removed as we switched to marked
  // In a full implementation we would configure marked slugger or use a custom renderer for headers
  // For now, TOC scrolling relies on ids that might not be present if we don't add them.
  // marked adds ids to headers by default (gfm: true). 
  // We can verify this or use a simple post-processing/custom renderer if needed.
  // For this "change parser" request, let's stick to default marked behavior first.

  return (
    <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4 animate-in fade-in duration-200">
      <div 
        className="bg-white dark:bg-gray-900 w-full max-w-6xl h-[90vh] rounded-2xl shadow-2xl flex flex-col overflow-hidden animate-in zoom-in-95 duration-200 border border-gray-200 dark:border-gray-800"
        onClick={(e) => e.stopPropagation()}
      >
        {/* Header */}
        <div className="flex items-center justify-between px-6 py-4 border-b border-gray-100 dark:border-gray-800 bg-white/50 dark:bg-gray-900/50 backdrop-blur shrink-0">
          <div className="flex items-center gap-3">
            <div className="p-2 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
                <svg className="w-5 h-5 text-blue-600 dark:text-blue-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                    <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
                </svg>
            </div>
            <div>
                <h3 className="font-medium text-gray-900 dark:text-gray-100">Markdown Preview</h3>
                <p className="text-xs text-gray-500 dark:text-gray-400">Read-only mode</p>
            </div>
          </div>
          <div className="flex items-center gap-2">
            {/* TOC Toggle */}
            {toc.length > 0 && (
                <button 
                    onClick={() => setShowToc(!showToc)}
                    className={`p-2 rounded-lg transition-colors flex items-center gap-2 ${
                        showToc 
                        ? 'text-blue-600 bg-blue-50 dark:text-blue-400 dark:bg-blue-900/20' 
                        : 'text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800'
                    }`}
                    title={showToc ? "Hide Table of Contents" : "Show Table of Contents"}
                >
                    {showToc ? <PanelRightClose size={18} /> : <PanelRightOpen size={18} />}
                </button>
            )}

            <div className="w-px h-6 bg-gray-200 dark:bg-gray-700 mx-1"></div>

            <button 
                onClick={handleCopy}
                className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors flex items-center gap-2"
                title="Copy content"
            >
                {copied ? <Check size={18} className="text-green-500" /> : <Copy size={18} />}
            </button>
            <button 
                onClick={handleDownload}
                className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors"
                title="Download .md"
            >
                <Download size={18} />
            </button>
            <div className="w-px h-6 bg-gray-200 dark:bg-gray-700 mx-1"></div>
            <button 
                onClick={onClose}
                className="p-2 text-gray-500 hover:text-red-600 dark:text-gray-400 dark:hover:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-lg transition-colors"
            >
                <X size={20} />
            </button>
          </div>
        </div>

        {/* Body */}
        <div className="flex-1 flex overflow-hidden">
            {/* Content */}
            <div className="flex-1 overflow-y-auto custom-scrollbar p-8 bg-white dark:bg-gray-900">
                <div className="prose dark:prose-invert max-w-none prose-headings:font-semibold prose-a:text-blue-600 dark:prose-a:text-blue-400 prose-code:text-blue-600 dark:prose-code:text-blue-400 prose-pre:bg-gray-50 dark:prose-pre:bg-gray-800 prose-pre:border prose-pre:border-gray-100 dark:prose-pre:border-gray-700 isolate">
                    <MarkdownRenderer 
                        content={content}
                    />
                </div>
            </div>

            {/* TOC Sidebar */}
            {toc.length > 0 && showToc && (
                <div className="w-64 border-l border-gray-200 dark:border-gray-800 bg-gray-50/50 dark:bg-gray-900/50 backdrop-blur overflow-y-auto custom-scrollbar p-4 animate-in slide-in-from-right-5 duration-200">
                    <h4 className="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-4">
                        Table of Contents
                    </h4>
                    <nav className="space-y-1">
                        {toc.map((item, index) => (
                            <button
                                key={`${item.id}-${index}`}
                                onClick={() => scrollToHeader(item.id)}
                                className={`
                                    block w-full text-left px-2 py-1.5 rounded text-sm transition-colors
                                    ${item.level === 1 ? 'font-medium text-gray-900 dark:text-gray-100' : ''}
                                    ${item.level === 2 ? 'pl-4 text-gray-600 dark:text-gray-400' : ''}
                                    ${item.level === 3 ? 'pl-8 text-gray-500 dark:text-gray-500' : ''}
                                    hover:bg-gray-100 dark:hover:bg-gray-800
                                `}
                            >
                                {item.text}
                            </button>
                        ))}
                    </nav>
                </div>
            )}
        </div>
      </div>
    </div>
  );
}