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:
161
public/js/views/dashboard.js
Normal file
161
public/js/views/dashboard.js
Normal file
@@ -0,0 +1,161 @@
|
||||
// Dashboard View
|
||||
|
||||
import { api } from '../api.js';
|
||||
import { renderSidebar } from '../components/sidebar.js';
|
||||
|
||||
export async function renderDashboard(app) {
|
||||
let documents = [];
|
||||
let libraries = [];
|
||||
let tags = [];
|
||||
let searchQuery = '';
|
||||
let selectedTag = null;
|
||||
let selectedLibrary = null;
|
||||
|
||||
try {
|
||||
[documents, libraries, tags] = await Promise.all([
|
||||
api.getDocuments(),
|
||||
api.getLibraries(),
|
||||
api.getTags()
|
||||
]);
|
||||
} catch (e) {
|
||||
app.showToast('Failed to load data', 'error');
|
||||
}
|
||||
|
||||
const appEl = document.getElementById('app');
|
||||
|
||||
function render() {
|
||||
let filteredDocs = documents;
|
||||
|
||||
if (searchQuery) {
|
||||
const q = searchQuery.toLowerCase();
|
||||
filteredDocs = filteredDocs.filter(d =>
|
||||
d.title.toLowerCase().includes(q) ||
|
||||
(d.content && d.content.toLowerCase().includes(q))
|
||||
);
|
||||
}
|
||||
|
||||
if (selectedTag) {
|
||||
filteredDocs = filteredDocs.filter(d =>
|
||||
d.tags && d.tags.includes(selectedTag)
|
||||
);
|
||||
}
|
||||
|
||||
if (selectedLibrary) {
|
||||
filteredDocs = filteredDocs.filter(d =>
|
||||
d.libraryId === selectedLibrary
|
||||
);
|
||||
}
|
||||
|
||||
appEl.innerHTML = `
|
||||
<header class="app-header">
|
||||
<div class="logo">📝 SimpleNote</div>
|
||||
<div class="search-box">
|
||||
<span class="icon">🔍</span>
|
||||
<input type="text" id="search-input" placeholder="Search documents..." value="${searchQuery}">
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<button class="btn btn-ghost btn-icon-only" onclick="window.app.navigate('editor')" title="New Document">+</button>
|
||||
<button class="btn btn-ghost btn-icon-only" onclick="window.app.navigate('editor', {libraryId: 'new'})" title="New Library">📁</button>
|
||||
</div>
|
||||
</header>
|
||||
<div class="app-layout">
|
||||
${renderSidebar({
|
||||
libraries,
|
||||
tags,
|
||||
selectedLibrary,
|
||||
selectedTag,
|
||||
onSelectLibrary: (id) => {
|
||||
selectedLibrary = id;
|
||||
selectedTag = null;
|
||||
render();
|
||||
},
|
||||
onSelectTag: (tag) => {
|
||||
selectedTag = tag;
|
||||
selectedLibrary = null;
|
||||
render();
|
||||
},
|
||||
onHome: () => {
|
||||
selectedTag = null;
|
||||
selectedLibrary = null;
|
||||
render();
|
||||
}
|
||||
})}
|
||||
<main class="main-content">
|
||||
<div class="content-header">
|
||||
<h1>${selectedLibrary ? getLibraryName(libraries, selectedLibrary) : selectedTag ? `#${selectedTag}` : 'All Documents'}</h1>
|
||||
<div class="header-actions">
|
||||
<button class="btn btn-primary" onclick="window.app.navigate('editor')">+ New Document</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content-body">
|
||||
${filteredDocs.length === 0 ? `
|
||||
<div class="empty-state">
|
||||
<div class="icon">📄</div>
|
||||
<h3>No documents found</h3>
|
||||
<p>${searchQuery || selectedTag ? 'Try adjusting your filters' : 'Create your first document'}</p>
|
||||
<button class="btn btn-primary" onclick="window.app.navigate('editor')">+ Create Document</button>
|
||||
</div>
|
||||
` : `
|
||||
<div class="doc-grid">
|
||||
${filteredDocs.map(doc => renderDocCard(doc)).join('')}
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Event listeners
|
||||
const searchInput = document.getElementById('search-input');
|
||||
searchInput.oninput = (e) => {
|
||||
searchQuery = e.target.value;
|
||||
render();
|
||||
};
|
||||
}
|
||||
|
||||
render();
|
||||
}
|
||||
|
||||
function getLibraryName(libraries, id) {
|
||||
const lib = libraries.find(l => l.id === id);
|
||||
return lib ? lib.name : 'Unknown';
|
||||
}
|
||||
|
||||
function renderDocCard(doc) {
|
||||
const priorityEmoji = { high: '🔴', medium: '🟡', low: '🟢' };
|
||||
const priority = doc.priority || 'medium';
|
||||
|
||||
return `
|
||||
<div class="doc-card" onclick="window.app.navigate('document', {id: '${doc.id}'})">
|
||||
<div class="doc-card-header">
|
||||
<span class="doc-id">${doc.id}</span>
|
||||
<span class="type-badge ${doc.type || 'general'}">${doc.type || 'general'}</span>
|
||||
</div>
|
||||
<h3 class="doc-title">${escapeHtml(doc.title)}</h3>
|
||||
${doc.tags && doc.tags.length ? `
|
||||
<div class="doc-tags">
|
||||
${doc.tags.map(t => `<span class="tag-pill">${escapeHtml(t)}</span>`).join('')}
|
||||
</div>
|
||||
` : ''}
|
||||
<div class="doc-meta">
|
||||
<span class="doc-meta-item">📅 ${formatDate(doc.createdAt)}</span>
|
||||
<span class="doc-meta-item">👤 ${escapeHtml(doc.author || 'unknown')}</span>
|
||||
<span class="status-badge ${doc.status || 'draft'}">${doc.status || 'draft'}</span>
|
||||
<span class="priority-indicator">${priorityEmoji[priority]}</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
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' });
|
||||
}
|
||||
Reference in New Issue
Block a user