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:
46
public/js/components/modal.js
Normal file
46
public/js/components/modal.js
Normal file
@@ -0,0 +1,46 @@
|
||||
// Modal Component
|
||||
|
||||
export function showModal({ title, content, onConfirm, onCancel, confirmText = 'Confirm', cancelText = 'Cancel', danger = false }) {
|
||||
const backdrop = document.createElement('div');
|
||||
backdrop.className = 'modal-backdrop';
|
||||
backdrop.innerHTML = `
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
<h3>${title}</h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
${content}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-ghost" id="modal-cancel">${cancelText}</button>
|
||||
<button class="btn ${danger ? 'btn-danger' : 'btn-primary'}" id="modal-confirm">${confirmText}</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(backdrop);
|
||||
|
||||
backdrop.querySelector('#modal-cancel').onclick = () => {
|
||||
backdrop.remove();
|
||||
if (onCancel) onCancel();
|
||||
};
|
||||
|
||||
backdrop.querySelector('#modal-confirm').onclick = () => {
|
||||
backdrop.remove();
|
||||
if (onConfirm) onConfirm();
|
||||
};
|
||||
|
||||
backdrop.onclick = (e) => {
|
||||
if (e.target === backdrop) {
|
||||
backdrop.remove();
|
||||
if (onCancel) onCancel();
|
||||
}
|
||||
};
|
||||
|
||||
return backdrop;
|
||||
}
|
||||
|
||||
export function hideModal(backdrop) {
|
||||
if (backdrop && backdrop.parentElement) {
|
||||
backdrop.remove();
|
||||
}
|
||||
}
|
||||
87
public/js/components/sidebar.js
Normal file
87
public/js/components/sidebar.js
Normal file
@@ -0,0 +1,87 @@
|
||||
// Sidebar Component
|
||||
|
||||
export function renderSidebar({ libraries, tags, selectedLibrary, selectedTag, onSelectLibrary, onSelectTag, onHome }) {
|
||||
const buildLibraryTree = (libs, parentId = null, depth = 0) => {
|
||||
return libs
|
||||
.filter(l => l.parentId === parentId)
|
||||
.map(lib => {
|
||||
const children = libs.filter(l => l.parentId === lib.id);
|
||||
const hasChildren = children.length > 0;
|
||||
const isSelected = selectedLibrary === lib.id;
|
||||
|
||||
return `
|
||||
<div class="tree-node">
|
||||
<div class="tree-item ${isSelected ? 'active' : ''}" onclick="handleSelectLibrary('${lib.id}')">
|
||||
<span class="tree-toggle ${hasChildren ? 'expanded' : ''}" style="padding-left:${depth * 12}px">
|
||||
${hasChildren ? '▶' : ''}
|
||||
</span>
|
||||
<span class="icon">📁</span>
|
||||
<span class="label">${escapeHtml(lib.name)}</span>
|
||||
</div>
|
||||
${hasChildren ? `<div class="tree-children">${buildLibraryTree(libraries, lib.id, depth + 1)}</div>` : ''}
|
||||
</div>
|
||||
`;
|
||||
})
|
||||
.join('');
|
||||
};
|
||||
|
||||
return `
|
||||
<aside class="sidebar">
|
||||
<div class="sidebar-scroll">
|
||||
<div class="sidebar-section">
|
||||
<h3>📚 Libraries</h3>
|
||||
<div class="library-tree">
|
||||
<div class="tree-item ${!selectedLibrary ? 'active' : ''}" onclick="handleHome()">
|
||||
<span class="icon">🏠</span>
|
||||
<span class="label">All Documents</span>
|
||||
</div>
|
||||
${buildLibraryTree(libraries)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="sidebar-section">
|
||||
<h3>🏷️ Tags</h3>
|
||||
<div class="tag-list">
|
||||
${tags.map(tag => `
|
||||
<div class="tag-item ${selectedTag === tag.name ? 'active' : ''}" onclick="handleSelectTag('${escapeHtml(tag.name)}')">
|
||||
<span>#${escapeHtml(tag.name)}</span>
|
||||
<span class="tag-count">${tag.count}</span>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
<div class="quick-links">
|
||||
<a class="quick-link" onclick="handleHome()">📋 All Documents</a>
|
||||
<a class="quick-link" onclick="window.app.navigate('editor')">+ New Document</a>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
<script>
|
||||
window.handleSelectLibrary = (id) => {
|
||||
${onSelectLibrary ? `window.document.dispatchEvent(new CustomEvent('select-library', {detail: '${''}'}))` : ''}
|
||||
};
|
||||
window.handleSelectTag = (tag) => {
|
||||
${onSelectTag ? `window.document.dispatchEvent(new CustomEvent('select-tag', {detail: tag}))` : ''}
|
||||
};
|
||||
window.handleHome = () => {
|
||||
${onHome ? `window.document.dispatchEvent(new CustomEvent('go-home'))` : ''}
|
||||
};
|
||||
|
||||
window.document.addEventListener('select-library', (e) => {
|
||||
${onSelectLibrary ? onSelectLibrary.toString().replace(/\s+/g, ' ') : ''}
|
||||
});
|
||||
window.document.addEventListener('select-tag', (e) => {
|
||||
${onSelectTag ? onSelectTag.toString().replace(/\s+/g, ' ') : ''}
|
||||
});
|
||||
window.document.addEventListener('go-home', (e) => {
|
||||
${onHome ? onHome.toString().replace(/\s+/g, ' ') : ''}
|
||||
});
|
||||
</script>
|
||||
`;
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
if (!str) return '';
|
||||
const div = document.createElement('div');
|
||||
div.textContent = str;
|
||||
return div.innerHTML;
|
||||
}
|
||||
Reference in New Issue
Block a user