Skip to main content

max / makenotwork

5.7 KB · 141 lines History Blame Raw
1 <div class="text-editor" id="text-editor">
2 <div class="editor-tabs">
3 <button type="button" class="editor-tab is-selected" data-tab="write" onclick="switchEditorTab('write')">Write</button>
4 <button type="button" class="editor-tab" data-tab="preview" onclick="switchEditorTab('preview')">Preview</button>
5 </div>
6
7 <div class="editor-panel active" id="write-panel">
8 <textarea id="text-body" name="body" placeholder="Write your content in Markdown...">{{ body.as_deref().unwrap_or("") }}</textarea>
9 <button type="button" class="btn-secondary text-editor-insert-btn" onclick="mediaPickerOpen('text-body')">Insert Image</button>
10 <div class="editor-footer">
11 <span class="word-count" id="word-count">{{ word_count.unwrap_or(0) }} words</span>
12 <span class="reading-time">{{ reading_time_minutes.unwrap_or(1) }} min read</span>
13 </div>
14 </div>
15
16 <div class="editor-panel" id="preview-panel">
17 <div class="markdown-preview" id="markdown-preview">
18 <p class="text-editor-placeholder">Preview will appear here...</p>
19 </div>
20 </div>
21
22 <div class="text-editor-actions">
23 <button type="button" class="btn-primary" id="save-text-btn" onclick="saveTextContent()">
24 Save Content
25 <span id="text-spinner" class="htmx-indicator"> ...</span>
26 </button>
27 <span id="text-save-status"></span>
28 </div>
29 </div>
30
31 <script>
32 function switchEditorTab(tab) {
33 document.querySelectorAll('.editor-tab').forEach(t => t.classList.remove('is-selected'));
34 document.querySelector('.editor-tab[data-tab="' + tab + '"]').classList.add('is-selected');
35
36 document.querySelectorAll('.editor-panel').forEach(p => p.classList.remove('active'));
37 document.getElementById(tab + '-panel').classList.add('active');
38
39 if (tab === 'preview') {
40 renderMarkdownPreview();
41 }
42 }
43
44 function renderMarkdownPreview() {
45 const body = document.getElementById('text-body').value;
46 const preview = document.getElementById('markdown-preview');
47
48 // Basic markdown rendering (paragraphs, bold, italic, headers)
49 let html = body
50 .split('\n\n').map(p => p.trim()).filter(p => p).map(p => {
51 // Headers
52 if (p.startsWith('### ')) return '<h3>' + escapeHtml(p.slice(4)) + '</h3>';
53 if (p.startsWith('## ')) return '<h2>' + escapeHtml(p.slice(3)) + '</h2>';
54 if (p.startsWith('# ')) return '<h1>' + escapeHtml(p.slice(2)) + '</h1>';
55 // Paragraph with inline formatting
56 let text = escapeHtml(p);
57 text = text.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
58 text = text.replace(/\*(.+?)\*/g, '<em>$1</em>');
59 text = text.replace(/`(.+?)`/g, '<code>$1</code>');
60 return '<p>' + text + '</p>';
61 }).join('\n');
62
63 preview.innerHTML = html || '<p class="text-editor-placeholder">Nothing to preview...</p>';
64 }
65
66 function escapeHtml(text) {
67 const div = document.createElement('div');
68 div.textContent = text;
69 return div.innerHTML;
70 }
71
72 function updateWordCount() {
73 const body = document.getElementById('text-body').value;
74 const words = body.trim().split(/\s+/).filter(w => w).length;
75 const readingTime = Math.max(1, Math.ceil(words / 200));
76 document.getElementById('word-count').textContent = words + ' words';
77 document.querySelector('.reading-time').textContent = readingTime + ' min read';
78 }
79
80 function saveTextContent() {
81 const body = document.getElementById('text-body').value;
82 const btn = document.getElementById('save-text-btn');
83 const status = document.getElementById('text-save-status');
84 const itemId = '{{ item.id }}';
85
86 btn.disabled = true;
87 status.innerHTML = '';
88
89 fetch('/api/items/' + itemId + '/text', {
90 method: 'PUT',
91 headers: { 'Content-Type': 'application/json', ...csrfHeaders() },
92 body: JSON.stringify({ body: body })
93 })
94 .then(res => res.json())
95 .then(data => {
96 status.innerHTML = '<span class="save-status success">Saved</span>';
97 if (data.word_count !== undefined) {
98 document.getElementById('word-count').textContent = data.word_count + ' words';
99 }
100 })
101 .catch(err => {
102 status.innerHTML = '<span class="save-status error">Error saving</span>';
103 })
104 .finally(() => {
105 btn.disabled = false;
106 });
107 }
108
109 document.getElementById('text-body').addEventListener('input', updateWordCount);
110
111 // Auto-save: debounce text body changes (30s after last keystroke)
112 var autoSaveTimer = null;
113 var autoSaveStatus = document.getElementById('text-save-status');
114
115 document.getElementById('text-body').addEventListener('input', function() {
116 clearTimeout(autoSaveTimer);
117 autoSaveTimer = setTimeout(function() {
118 autoSaveStatus.innerHTML = '<span class="save-status saving">Saving...</span>';
119 var body = document.getElementById('text-body').value;
120 fetch('/api/items/{{ item.id }}/text', {
121 method: 'PUT',
122 headers: { 'Content-Type': 'application/json', ...csrfHeaders() },
123 body: JSON.stringify({ body: body })
124 })
125 .then(function(res) { return res.json(); })
126 .then(function(data) {
127 autoSaveStatus.innerHTML = '<span class="save-status success">Auto-saved</span>';
128 if (data.word_count !== undefined) {
129 document.getElementById('word-count').textContent = data.word_count + ' words';
130 }
131 setTimeout(function() {
132 if (autoSaveStatus.textContent === 'Auto-saved') autoSaveStatus.innerHTML = '';
133 }, 3000);
134 })
135 .catch(function() {
136 autoSaveStatus.innerHTML = '<span class="save-status error">Auto-save failed</span>';
137 });
138 }, 30000);
139 });
140 </script>
141