171 lines
5.8 KiB
JavaScript
171 lines
5.8 KiB
JavaScript
// Document View
|
|
|
|
import { api } from '../api.js';
|
|
|
|
export async function renderDocument(app) {
|
|
const { id, projectId } = app.state.params;
|
|
let doc;
|
|
|
|
try {
|
|
doc = await api.getDocument(id);
|
|
} catch (e) {
|
|
app.showToast('Failed to load document', 'error');
|
|
app.navigate('projects');
|
|
return;
|
|
}
|
|
|
|
const backToProject = () => {
|
|
if (projectId) {
|
|
app.navigate('project', { id: projectId });
|
|
} else {
|
|
app.navigate('projects');
|
|
}
|
|
};
|
|
|
|
const appEl = document.getElementById('app');
|
|
|
|
function render() {
|
|
const priorityEmoji = { high: '🔴', medium: '🟡', low: '🟢' };
|
|
const priority = doc.priority || 'medium';
|
|
const renderedContent = renderMarkdown(doc.content || '');
|
|
|
|
appEl.innerHTML = `
|
|
<header class="app-header">
|
|
<button type="button" class="btn btn-ghost" onclick="backToProject()">← Back</button>
|
|
<div class="header-actions">
|
|
<button type="button" class="btn btn-ghost" onclick="window.app.navigate('editor', {id: '${doc.id}'})">✏️ Edit</button>
|
|
<button type="button" class="btn btn-ghost" onclick="exportDoc()">📥 Export</button>
|
|
<button type="button" class="btn btn-ghost danger" onclick="deleteDoc()">🗑️ Delete</button>
|
|
</div>
|
|
</header>
|
|
<main class="main-content">
|
|
<div class="content-body">
|
|
<div class="doc-viewer">
|
|
<div class="doc-content">
|
|
<div class="doc-viewer-header">
|
|
<span class="doc-id">${doc.id}</span>
|
|
<span class="type-badge ${doc.type || 'general'}">${doc.type || 'general'}</span>
|
|
</div>
|
|
<div class="prose">${renderedContent}</div>
|
|
</div>
|
|
<aside class="doc-sidebar">
|
|
<div class="meta-section">
|
|
<div class="meta-header">Details</div>
|
|
<div class="meta-body">
|
|
<div class="meta-row">
|
|
<span class="meta-label">Status</span>
|
|
<span class="status-badge ${doc.status || 'draft'}">${doc.status || 'draft'}</span>
|
|
</div>
|
|
<div class="meta-row">
|
|
<span class="meta-label">Priority</span>
|
|
<span class="meta-value">${priorityEmoji[priority]} ${priority}</span>
|
|
</div>
|
|
<div class="meta-row">
|
|
<span class="meta-label">Author</span>
|
|
<span class="meta-value">${escapeHtml(doc.author || 'unknown')}</span>
|
|
</div>
|
|
<div class="meta-row">
|
|
<span class="meta-label">Created</span>
|
|
<span class="meta-value">${formatDate(doc.createdAt)}</span>
|
|
</div>
|
|
<div class="meta-row">
|
|
<span class="meta-label">Updated</span>
|
|
<span class="meta-value">${formatDate(doc.updatedAt)}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
${doc.tags && doc.tags.length ? `
|
|
<div class="meta-section">
|
|
<div class="meta-header">Tags</div>
|
|
<div class="meta-body doc-tags">
|
|
${doc.tags.map(t => `<span class="tag-pill" onclick="filterByTag('${escapeHtml(t)}'); event.stopPropagation();">${escapeHtml(t)}</span>`).join('')}
|
|
</div>
|
|
</div>
|
|
` : ''}
|
|
</aside>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
`;
|
|
|
|
window.filterByTag = (tag) => {
|
|
// Store the tag to filter by in app state so dashboard can pick it up
|
|
app.state.selectedTag = tag;
|
|
app.state.selectedLibrary = null;
|
|
backToProject();
|
|
};
|
|
}
|
|
|
|
render();
|
|
|
|
async function exportDoc() {
|
|
try {
|
|
const markdown = await api.exportDocument(id);
|
|
const blob = new Blob([markdown], { type: 'text/markdown' });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
// Sanitize filename to prevent path traversal
|
|
const safeFilename = (doc.title || 'untitled')
|
|
.replace(/[^a-zA-Z0-9_\-\s]/g, '')
|
|
.replace(/\s+/g, '-')
|
|
.substring(0, 100);
|
|
a.download = `${doc.id}-${safeFilename}.md`;
|
|
a.click();
|
|
URL.revokeObjectURL(url);
|
|
app.showToast('Document exported', 'success');
|
|
} catch (e) {
|
|
app.showToast('Failed to export', 'error');
|
|
}
|
|
}
|
|
|
|
async function deleteDoc() {
|
|
const confirmed = await app.confirmDelete(`Delete "${doc.title}"? This cannot be undone.`);
|
|
if (confirmed) {
|
|
try {
|
|
await api.deleteDocument(id);
|
|
app.showToast('Document deleted', 'success');
|
|
backToProject();
|
|
} catch (e) {
|
|
app.showToast('Failed to delete', 'error');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function escapeHtml(str) {
|
|
if (!str) return '';
|
|
const div = document.createElement('div');
|
|
div.textContent = str;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
function formatDate(dateStr) {
|
|
if (!dateStr) return '';
|
|
const date = new Date(dateStr);
|
|
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
|
|
}
|
|
|
|
function renderMarkdown(content) {
|
|
// Simple markdown rendering using marked library if available
|
|
if (typeof marked !== 'undefined') {
|
|
return marked.parse(content);
|
|
}
|
|
|
|
// Fallback simple rendering
|
|
return content
|
|
.replace(/^### (.+)$/gm, '<h3>$1</h3>')
|
|
.replace(/^## (.+)$/gm, '<h2>$1</h2>')
|
|
.replace(/^# (.+)$/gm, '<h1>$1</h1>')
|
|
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
|
.replace(/\*(.+?)\*/g, '<em>$1</em>')
|
|
.replace(/`(.+?)`/g, '<code>$1</code>')
|
|
.replace(/^- (.+)$/gm, '<li>$1</li>')
|
|
.replace(/(<li>.*<\/li>)/s, '<ul>$1</ul>')
|
|
.replace(/\n\n/g, '</p><p>')
|
|
.replace(/^(.+)$/gm, (match) => {
|
|
if (match.startsWith('<')) return match;
|
|
return `<p>${match}</p>`;
|
|
});
|
|
}
|