feat: add frontend UI for SimpleNote Web
- Vanilla JS frontend with dark theme - Dashboard with sidebar (libraries tree, tags), document grid, search - Document viewer with markdown rendering and metadata panel - Document editor with split write/preview and formatting toolbar - Login screen with token authentication - All styled according to UI/UX specs (dark theme, accent #00d4aa) - API client for all endpoints - Responsive design
This commit is contained in:
154
public/js/views/document.js
Normal file
154
public/js/views/document.js
Normal file
@@ -0,0 +1,154 @@
|
||||
// Document View
|
||||
|
||||
import { api } from '../api.js';
|
||||
|
||||
export async function renderDocument(app) {
|
||||
const { id } = app.state.params;
|
||||
let doc;
|
||||
|
||||
try {
|
||||
doc = await api.getDocument(id);
|
||||
} catch (e) {
|
||||
app.showToast('Failed to load document', 'error');
|
||||
app.navigate('dashboard');
|
||||
return;
|
||||
}
|
||||
|
||||
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 class="btn btn-ghost" onclick="window.app.navigate('dashboard')">← Back</button>
|
||||
<div class="header-actions">
|
||||
<button class="btn btn-ghost" onclick="window.app.navigate('editor', {id: '${doc.id}'})">✏️ Edit</button>
|
||||
<button class="btn btn-ghost" onclick="exportDoc()">📥 Export</button>
|
||||
<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)}')">${escapeHtml(t)}</span>`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
`;
|
||||
|
||||
window.filterByTag = (tag) => {
|
||||
app.navigate('dashboard');
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
||||
a.download = `${doc.id}-${doc.title}.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');
|
||||
app.navigate('dashboard');
|
||||
} 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>`;
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user