- Add mobile navigation drawer with hamburger menu - Sidebar slides in as overlay on mobile (<768px) - Close button inside sidebar for mobile - Ensure touch targets are at least 44px on mobile - Make modals full-screen on mobile - Editor toolbar scrolls horizontally on mobile - Improve spacing and typography for small screens - Keep dark theme consistent across breakpoints - Projects page cards stack vertically on mobile - Document cards full-width on mobile
391 lines
13 KiB
JavaScript
391 lines
13 KiB
JavaScript
// 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 = app.state.selectedTag || null;
|
|
let selectedLibrary = app.state.selectedLibrary || null;
|
|
|
|
try {
|
|
const [docResult, libResult, tagResult] = await Promise.all([
|
|
api.getDocuments(),
|
|
api.getLibraries(),
|
|
api.getTags()
|
|
]);
|
|
documents = docResult.documents || [];
|
|
libraries = libResult.libraries || [];
|
|
tags = tagResult.tags || [];
|
|
} catch (e) {
|
|
app.showToast('Failed to load data', 'error');
|
|
}
|
|
|
|
const appEl = document.getElementById('app');
|
|
|
|
function render() {
|
|
// Store callbacks for sidebar event handlers
|
|
const sidebarCallbacks = {
|
|
onSelectLibrary: (id) => {
|
|
selectedLibrary = id;
|
|
selectedTag = null;
|
|
app.state.selectedLibrary = id;
|
|
app.state.selectedTag = null;
|
|
render();
|
|
},
|
|
onSelectTag: (tag) => {
|
|
selectedTag = tag;
|
|
selectedLibrary = null;
|
|
app.state.selectedTag = tag;
|
|
app.state.selectedLibrary = null;
|
|
render();
|
|
},
|
|
onHome: () => {
|
|
selectedTag = null;
|
|
selectedLibrary = null;
|
|
app.state.selectedTag = null;
|
|
app.state.selectedLibrary = null;
|
|
render();
|
|
}
|
|
};
|
|
window.__sidebarCallbacks = sidebarCallbacks;
|
|
|
|
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">
|
|
<button class="mobile-nav-btn" onclick="toggleMobileSidebar()" title="Menu">☰</button>
|
|
<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-primary" onclick="window.showNewDocModal()">+ New</button>
|
|
</div>
|
|
</header>
|
|
<div class="sidebar-overlay" onclick="closeMobileSidebar()"></div>
|
|
<div class="app-layout">
|
|
<aside class="sidebar" id="sidebar">
|
|
<button class="sidebar-close-btn" onclick="closeMobileSidebar()">✕</button>
|
|
${renderSidebar({
|
|
libraries,
|
|
tags,
|
|
selectedLibrary,
|
|
selectedTag,
|
|
...sidebarCallbacks
|
|
})}
|
|
</aside>
|
|
<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.showNewDocModal()">+ New</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.showNewDocModal()">+ Create Document</button>
|
|
</div>
|
|
` : `
|
|
<div class="doc-grid">
|
|
${filteredDocs.map(doc => renderDocCard(doc)).join('')}
|
|
</div>
|
|
`}
|
|
</div>
|
|
</main>
|
|
</div>
|
|
`;
|
|
|
|
// Mobile sidebar functions
|
|
window.toggleMobileSidebar = function() {
|
|
const sidebar = document.getElementById('sidebar');
|
|
const overlay = document.querySelector('.sidebar-overlay');
|
|
if (sidebar) {
|
|
sidebar.classList.toggle('mobile-open');
|
|
if (overlay) overlay.classList.toggle('active');
|
|
}
|
|
};
|
|
|
|
window.closeMobileSidebar = function() {
|
|
const sidebar = document.getElementById('sidebar');
|
|
const overlay = document.querySelector('.sidebar-overlay');
|
|
if (sidebar) {
|
|
sidebar.classList.remove('mobile-open');
|
|
if (overlay) overlay.classList.remove('active');
|
|
}
|
|
};
|
|
|
|
// Event listeners
|
|
const searchInput = document.getElementById('search-input');
|
|
searchInput.oninput = (e) => {
|
|
searchQuery = e.target.value;
|
|
render();
|
|
};
|
|
|
|
// Sidebar item listeners
|
|
document.querySelectorAll('[data-action="home"]').forEach(el => {
|
|
el.addEventListener('click', (e) => {
|
|
e.stopPropagation();
|
|
sidebarCallbacks.onHome();
|
|
closeMobileSidebar();
|
|
});
|
|
});
|
|
document.querySelectorAll('[data-action="library"]').forEach(el => {
|
|
el.addEventListener('click', (e) => {
|
|
e.stopPropagation();
|
|
sidebarCallbacks.onSelectLibrary(el.getAttribute('data-library-id'));
|
|
closeMobileSidebar();
|
|
});
|
|
});
|
|
document.querySelectorAll('[data-action="tag"]').forEach(el => {
|
|
el.addEventListener('click', (e) => {
|
|
e.stopPropagation();
|
|
sidebarCallbacks.onSelectTag(el.getAttribute('data-tag'));
|
|
closeMobileSidebar();
|
|
});
|
|
});
|
|
}
|
|
|
|
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' });
|
|
}
|
|
|
|
// Global function: Show modal to create new document (with library selection)
|
|
window.showNewDocModal = async function() {
|
|
let libraries = [];
|
|
try {
|
|
const libResult = await api.getLibraries();
|
|
libraries = libResult.libraries || [];
|
|
} catch (e) {}
|
|
|
|
let step = 1; // 1 = choose library, 2 = create new library
|
|
let newLibraryName = '';
|
|
|
|
function render() {
|
|
const backdrop = document.createElement('div');
|
|
backdrop.className = 'modal-backdrop';
|
|
|
|
if (step === 1) {
|
|
backdrop.innerHTML = `
|
|
<div class="modal" style="min-width: 450px;">
|
|
<div class="modal-header">
|
|
<span>📄</span>
|
|
<h3>Create New Document</h3>
|
|
<button class="modal-close" onclick="this.closest('.modal-backdrop').remove()">✕</button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<p style="color: var(--color-text-secondary); margin-bottom: 16px;">Choose a library for your document:</p>
|
|
<div class="form-group">
|
|
<label for="doc-library-select">Library</label>
|
|
<select id="doc-library-select" class="form-control">
|
|
<option value="">— No Library —</option>
|
|
${libraries.map(l => `<option value="${l.id}">📁 ${escapeHtml(l.name)}</option>`).join('')}
|
|
<option value="__new__">+ Create New Library</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button class="btn btn-ghost" onclick="this.closest('.modal-backdrop').remove()">Cancel</button>
|
|
<button class="btn btn-primary" id="doc-next-btn">Next →</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
} else {
|
|
backdrop.innerHTML = `
|
|
<div class="modal" style="min-width: 450px;">
|
|
<div class="modal-header">
|
|
<span>📁</span>
|
|
<h3>Create New Library</h3>
|
|
<button class="modal-close" onclick="this.closest('.modal-backdrop').remove()">✕</button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="form-group">
|
|
<label for="new-library-name">Library Name</label>
|
|
<input type="text" id="new-library-name" class="form-control" placeholder="e.g., Backend Requirements" value="${escapeHtml(newLibraryName)}">
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button class="btn btn-ghost" id="doc-back-btn">← Back</button>
|
|
<button class="btn btn-primary" id="doc-create-lib-btn">Create Library</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
document.body.appendChild(backdrop);
|
|
|
|
if (step === 1) {
|
|
const select = document.getElementById('doc-library-select');
|
|
const nextBtn = document.getElementById('doc-next-btn');
|
|
|
|
nextBtn.onclick = () => {
|
|
const value = select.value;
|
|
if (value === '__new__') {
|
|
step = 2;
|
|
backdrop.remove();
|
|
render();
|
|
} else {
|
|
backdrop.remove();
|
|
window.app.navigate('editor', { libraryId: value || null });
|
|
}
|
|
};
|
|
} else {
|
|
const backBtn = document.getElementById('doc-back-btn');
|
|
const createBtn = document.getElementById('doc-create-lib-btn');
|
|
const nameInput = document.getElementById('new-library-name');
|
|
|
|
backBtn.onclick = () => {
|
|
newLibraryName = nameInput.value;
|
|
step = 1;
|
|
backdrop.remove();
|
|
render();
|
|
};
|
|
|
|
createBtn.onclick = async () => {
|
|
const name = nameInput.value.trim();
|
|
if (!name) {
|
|
window.app.showToast('Please enter a library name', 'error');
|
|
return;
|
|
}
|
|
try {
|
|
const result = await api.createLibrary({ name });
|
|
backdrop.remove();
|
|
window.app.showToast('Library created', 'success');
|
|
window.app.navigate('editor', { libraryId: result.id });
|
|
} catch (e) {
|
|
window.app.showToast('Failed to create library: ' + e.message, 'error');
|
|
}
|
|
};
|
|
}
|
|
|
|
backdrop.onclick = (e) => {
|
|
if (e.target === backdrop) backdrop.remove();
|
|
};
|
|
}
|
|
|
|
render();
|
|
};
|
|
|
|
// Global function: Show modal to create new library
|
|
window.showNewLibraryModal = function() {
|
|
const backdrop = document.createElement('div');
|
|
backdrop.className = 'modal-backdrop';
|
|
backdrop.innerHTML = `
|
|
<div class="modal" style="min-width: 450px;">
|
|
<div class="modal-header">
|
|
<span>📁</span>
|
|
<h3>Create New Library</h3>
|
|
<button class="modal-close" onclick="this.closest('.modal-backdrop').remove()">✕</button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<p style="color: var(--color-text-secondary); margin-bottom: 16px;">Libraries help you organize your documents.</p>
|
|
<div class="form-group">
|
|
<label for="new-lib-name">Library Name</label>
|
|
<input type="text" id="new-lib-name" class="form-control" placeholder="e.g., Backend Requirements">
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button class="btn btn-ghost" onclick="this.closest('.modal-backdrop').remove()">Cancel</button>
|
|
<button class="btn btn-primary" id="create-lib-btn">Create Library</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
document.body.appendChild(backdrop);
|
|
|
|
const nameInput = document.getElementById('new-lib-name');
|
|
const createBtn = document.getElementById('create-lib-btn');
|
|
|
|
createBtn.onclick = async () => {
|
|
const name = nameInput.value.trim();
|
|
if (!name) {
|
|
window.app.showToast('Please enter a library name', 'error');
|
|
return;
|
|
}
|
|
try {
|
|
await api.createLibrary({ name });
|
|
backdrop.remove();
|
|
window.app.showToast('Library created successfully', 'success');
|
|
window.app.navigate('dashboard');
|
|
} catch (e) {
|
|
window.app.showToast('Failed to create library: ' + e.message, 'error');
|
|
}
|
|
};
|
|
|
|
backdrop.onclick = (e) => {
|
|
if (e.target === backdrop) backdrop.remove();
|
|
};
|
|
|
|
nameInput.focus();
|
|
};
|