This commit is contained in:
pika 2025-04-17 15:36:24 +02:00
parent f5c8e9ee23
commit 3a16f266da
15 changed files with 511 additions and 169 deletions

View file

@ -56,49 +56,53 @@
<div class="flex min-h-screen {% if request.cookies.get('sidebar_collapsed') == 'true' %}sidebar-hidden{% endif %}">
<!-- Sidebar -->
<aside class="w-64 bg-gray-800 h-screen flex-shrink-0 fixed left-0 top-0 z-10 overflow-y-auto transition-all ease-in-out duration-300">
<div class="p-4 border-b border-gray-700 flex items-center">
<div class="p-4 border-b border-gray-700/30 flex items-center">
<h1 class="text-xl font-semibold text-white flex items-center">
<i class="mdi mdi-vim text-primary text-2xl mr-2"></i> Vim Docs
</h1>
</div>
<nav class="py-4">
<nav class="py-2">
<ul class="list-none">
<li class="my-1">
<a href="{{ url_for('main.index') }}" class="flex items-center px-4 py-2 text-gray-300 hover:text-primary hover:bg-gray-700 rounded-md transition-all {{ 'bg-primary/10 text-primary' if request.endpoint == 'main.index' }}">
<li>
<a href="{{ url_for('main.index') }}" class="flex items-center px-4 py-2 text-gray-300 hover:text-primary hover:bg-gray-700/50 rounded-md transition-all {{ 'bg-primary/10 text-primary' if request.endpoint == 'main.index' }}">
<i class="mdi mdi-home mr-3"></i> Home
</a>
</li>
<li class="my-4">
<div class="block font-medium text-gray-400 px-4 py-2 flex items-center">
<i class="mdi mdi-folder-multiple-outline mr-3"></i> Categories
</div>
<ul class="ml-3 pl-3 border-l border-gray-700 my-1" id="category-tree">
<li class="mt-3">
<a href="#" id="root-category-link" class="flex items-center justify-between px-4 py-2 text-gray-300 hover:text-primary hover:bg-gray-700/50 rounded-md transition-all">
<div class="flex items-center">
<i class="mdi mdi-folder-outline mr-3"></i>
<span>Files & Categories</span>
</div>
<i class="mdi mdi-chevron-down text-sm"></i>
</a>
<ul class="ml-3 pt-1" id="category-tree">
<!-- Categories will be loaded here via JS -->
</ul>
</li>
<li class="my-4">
<div class="block font-medium text-gray-400 px-4 py-2 flex items-center">
<li class="mt-3">
<div class="block font-medium text-gray-400 px-4 py-1 flex items-center">
<i class="mdi mdi-file-document-multiple-outline mr-3"></i> Documents
</div>
<ul class="ml-3 pl-3 border-l border-gray-700 my-1">
<ul class="ml-3 pt-1">
<li>
<a href="{{ url_for('main.new_document') }}" class="flex items-center py-1 px-2 text-gray-400 hover:text-primary rounded transition-colors {{ 'text-primary' if request.endpoint == 'main.new_document' }}">
<a href="{{ url_for('main.new_document') }}" class="flex items-center py-1 px-2 text-gray-400 hover:text-primary hover:bg-gray-700/30 rounded transition-colors {{ 'text-primary' if request.endpoint == 'main.new_document' }}">
<i class="mdi mdi-plus-circle-outline mr-2 text-sm"></i> New Document
</a>
</li>
<li>
<a href="{{ url_for('main.index') }}?view=recent" class="flex items-center py-1 px-2 text-gray-400 hover:text-primary rounded transition-colors">
<a href="{{ url_for('main.index') }}?view=recent" class="flex items-center py-1 px-2 text-gray-400 hover:text-primary hover:bg-gray-700/30 rounded transition-colors">
<i class="mdi mdi-clock-outline mr-2 text-sm"></i> Recent Documents
</a>
</li>
</ul>
</li>
<li class="my-1">
<a href="{{ url_for('main.new_document') }}" class="flex items-center px-4 py-2 bg-primary text-black hover:bg-primary-dark rounded-md transition-all">
<i class="mdi mdi-plus-circle mr-3"></i> New Document
<li class="mt-4 mb-2 px-3">
<a href="{{ url_for('main.new_document') }}" class="flex items-center justify-center px-4 py-2 bg-primary text-black hover:bg-primary-dark rounded-md transition-all">
<i class="mdi mdi-plus-circle mr-2"></i> New Document
</a>
</li>
</ul>
@ -188,6 +192,55 @@
});
}
// Root category toggle
const rootCategoryLink = document.getElementById('root-category-link');
const categoryTree = document.getElementById('category-tree');
if (rootCategoryLink && categoryTree) {
// Initialize - expand by default
rootCategoryLink.querySelector('i.mdi-chevron-down').classList.add('rotate-180');
rootCategoryLink.addEventListener('click', function(e) {
// Only handle the toggle if clicking on the chevron icon
if (e.target.classList.contains('mdi-chevron-down') || e.target.parentElement.classList.contains('mdi-chevron-down')) {
e.preventDefault();
e.stopPropagation();
// Toggle category tree visibility
if (categoryTree.classList.contains('hidden')) {
categoryTree.classList.remove('hidden');
rootCategoryLink.querySelector('i.mdi-chevron-down').classList.add('rotate-180');
} else {
categoryTree.classList.add('hidden');
rootCategoryLink.querySelector('i.mdi-chevron-down').classList.remove('rotate-180');
}
// Save preference in localStorage
localStorage.setItem('rootCategoryExpanded', !categoryTree.classList.contains('hidden'));
} else {
// If not clicking on the chevron, we need to load the root category
fetch('/api/categories')
.then(response => response.json())
.then(categories => {
const rootCategory = categories.find(c => c.is_root);
if (rootCategory) {
window.location.href = `/category/${rootCategory.id}`;
}
})
.catch(error => {
console.error('Error fetching root category:', error);
});
}
});
// Load saved preference
const rootExpanded = localStorage.getItem('rootCategoryExpanded') !== 'false';
if (!rootExpanded) {
categoryTree.classList.add('hidden');
rootCategoryLink.querySelector('i.mdi-chevron-down').classList.remove('rotate-180');
}
}
// Load categories
loadCategories();
@ -205,20 +258,155 @@
categoryTree.innerHTML = ''; // Clear existing items
if (categories.length === 0) {
categoryTree.innerHTML = '<li class="px-4 py-2 text-gray-500">No categories found</li>';
categoryTree.innerHTML = '<li class="px-4 py-2 text-gray-500">No items found</li>';
return;
}
categories.forEach(category => {
categoryTree.appendChild(createCategoryItem(category));
});
// First add documents without categories (directly in root)
const rootCategory = categories.find(c => c.is_root);
if (rootCategory && rootCategory.documents && rootCategory.documents.length > 0) {
// Create a section header for root documents
const docHeader = document.createElement('div');
docHeader.className = 'text-xs uppercase text-gray-500 font-medium px-2 py-1 mt-2';
docHeader.textContent = 'Files';
categoryTree.appendChild(docHeader);
// Create a container for documents
const docsContainer = document.createElement('div');
docsContainer.className = 'mb-2';
categoryTree.appendChild(docsContainer);
const documentsUl = document.createElement('ul');
documentsUl.className = 'py-1 space-y-0.5';
docsContainer.appendChild(documentsUl);
rootCategory.documents.forEach(docId => {
// Fetch document details and add to the tree
fetch(`/api/document/${docId}`)
.then(response => response.json())
.then(doc => {
const docLi = document.createElement('li');
docLi.className = 'document-item relative group';
const docLink = document.createElement('a');
docLink.href = `/document/${doc.id}`;
docLink.className = 'flex items-center py-1 px-2 text-gray-400 hover:text-primary hover:bg-gray-700/30 rounded transition-colors truncate';
docLink.innerHTML = `<i class="mdi mdi-file-document-outline mr-2 text-sm"></i> <span class="truncate">${doc.title}</span>`;
// Add drag functionality
docLink.draggable = true;
docLink.dataset.docId = doc.id;
docLink.addEventListener('dragstart', function(e) {
e.dataTransfer.setData('text/plain', JSON.stringify({
type: 'document',
id: doc.id,
title: doc.title
}));
docLi.classList.add('dragging');
});
docLink.addEventListener('dragend', function() {
docLi.classList.remove('dragging');
});
// Add document actions
const docActions = createDocumentActions(doc, docLi, rootCategory.id);
docLi.appendChild(docLink);
docLi.appendChild(docActions);
documentsUl.appendChild(docLi);
})
.catch(error => {
console.error(`Error fetching document ${docId}:`, error);
});
});
}
// If there are any non-root categories, add a section header
const nonRootCategories = categories.filter(c => !c.is_root);
if (nonRootCategories.length > 0) {
const catHeader = document.createElement('div');
catHeader.className = 'text-xs uppercase text-gray-500 font-medium px-2 py-1 mt-3';
catHeader.textContent = 'Categories';
categoryTree.appendChild(catHeader);
// Then add all non-root categories
nonRootCategories.forEach(category => {
categoryTree.appendChild(createCategoryItem(category));
});
}
})
.catch(error => {
console.error('Error loading categories:', error);
categoryTree.innerHTML = '<li class="px-4 py-2 text-red-500">Error loading categories</li>';
categoryTree.innerHTML = '<li class="px-4 py-2 text-red-500">Error loading items</li>';
});
}
function createDocumentActions(doc, docLi, categoryId) {
const docActions = document.createElement('div');
docActions.className = 'actions absolute right-0 hidden group-hover:flex items-center bg-gray-800/90 px-1 rounded-sm';
// Edit document button
const editBtn = document.createElement('button');
editBtn.className = 'p-1 text-gray-500 hover:text-primary rounded transition-colors';
editBtn.title = 'Edit document';
editBtn.innerHTML = '<i class="mdi mdi-pencil-outline text-sm"></i>';
editBtn.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
window.location.href = `/document/${doc.id}/edit`;
});
docActions.appendChild(editBtn);
// Delete document button
const deleteBtn = document.createElement('button');
deleteBtn.className = 'p-1 text-gray-500 hover:text-red-500 rounded transition-colors';
deleteBtn.title = 'Delete document';
deleteBtn.innerHTML = '<i class="mdi mdi-delete-outline text-sm"></i>';
deleteBtn.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
// Show confirmation dialog
if (confirm(`Are you sure you want to delete "${doc.title}"? This cannot be undone.`)) {
// Send delete request
fetch(`/api/document/${doc.id}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCsrfToken()
}
})
.then(response => {
if (!response.ok) {
throw new Error('Failed to delete');
}
return response.json();
})
.then(data => {
// Remove the document from the DOM
docLi.remove();
// Show notification
showNotification('Document deleted successfully');
// If we're on the document's page, redirect to the category or home
const currentPath = window.location.pathname;
if (currentPath === `/document/${doc.id}` || currentPath === `/document/${doc.id}/edit`) {
window.location.href = categoryId ? `/category/${categoryId}` : '/';
}
})
.catch(error => {
console.error('Error:', error);
showNotification('Error deleting document', 'error');
});
}
});
docActions.appendChild(deleteBtn);
return docActions;
}
function createCategoryItem(category) {
const li = document.createElement('li');
li.className = 'category-item my-1';
@ -248,7 +436,7 @@
// Create the link to view the category
const a = document.createElement('a');
a.href = `/category/${category.id}`;
let categoryClass = 'flex-grow flex items-center py-1 px-2 text-gray-400 hover:text-primary rounded transition-colors overflow-hidden';
let categoryClass = 'flex-grow flex items-center py-1 px-2 text-gray-400 hover:text-primary hover:bg-gray-700/30 rounded transition-colors overflow-hidden';
// Special styling for root
if (category.is_root) {
@ -264,7 +452,7 @@
// Create actions container that appears on hover
const actionsContainer = document.createElement('div');
actionsContainer.className = 'actions absolute right-0 hidden group-hover:flex items-center bg-gray-800 px-1';
actionsContainer.className = 'actions absolute right-0 hidden group-hover:flex items-center bg-gray-800/90 px-1 rounded-sm';
categoryContainer.appendChild(actionsContainer);
// Add document button - consistent across all views
@ -305,7 +493,7 @@
// Create the child container for documents and subcategories
const childrenContainer = document.createElement('div');
childrenContainer.className = 'ml-2 pl-2 border-l border-gray-700 mt-1 mb-1 overflow-hidden transition-all duration-300';
childrenContainer.className = 'ml-4 mt-1 mb-1 overflow-hidden transition-all duration-300';
childrenContainer.style.display = 'none'; // Initially collapsed
li.appendChild(childrenContainer);
@ -357,7 +545,7 @@
// Add documents first
if (hasDocuments) {
const documentsUl = document.createElement('ul');
documentsUl.className = 'py-1 space-y-1';
documentsUl.className = 'py-1 space-y-0.5';
childrenContainer.appendChild(documentsUl);
// Sort documents by name
@ -374,7 +562,7 @@
const docLink = document.createElement('a');
docLink.href = `/document/${doc.id}`;
docLink.className = 'flex items-center py-1 px-2 text-gray-400 hover:text-primary rounded transition-colors truncate';
docLink.className = 'flex items-center py-1 px-2 text-gray-400 hover:text-primary hover:bg-gray-700/30 rounded transition-colors truncate';
docLink.innerHTML = `<i class="mdi mdi-file-document-outline mr-2 text-sm"></i> <span class="truncate">${doc.title}</span>`;
// Add drag functionality
@ -394,66 +582,7 @@
});
// Add actions for documents
const docActions = document.createElement('div');
docActions.className = 'actions absolute right-0 hidden group-hover:flex items-center bg-gray-800 px-1';
// Edit document button
const editBtn = document.createElement('button');
editBtn.className = 'p-1 text-gray-500 hover:text-primary rounded transition-colors';
editBtn.title = 'Edit document';
editBtn.innerHTML = '<i class="mdi mdi-pencil-outline text-sm"></i>';
editBtn.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
window.location.href = `/document/${doc.id}/edit`;
});
docActions.appendChild(editBtn);
// Delete document button
const deleteBtn = document.createElement('button');
deleteBtn.className = 'p-1 text-gray-500 hover:text-red-500 rounded transition-colors';
deleteBtn.title = 'Delete document';
deleteBtn.innerHTML = '<i class="mdi mdi-delete-outline text-sm"></i>';
deleteBtn.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
// Show confirmation dialog
if (confirm(`Are you sure you want to delete "${doc.title}"? This cannot be undone.`)) {
// Send delete request
fetch(`/api/document/${doc.id}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCsrfToken()
}
})
.then(response => {
if (!response.ok) {
throw new Error('Failed to delete');
}
return response.json();
})
.then(data => {
// Remove the document from the DOM
docLi.remove();
// Show notification
showNotification('Document deleted successfully');
// If we're on the document's page, redirect to the category or home
const currentPath = window.location.pathname;
if (currentPath === `/document/${doc.id}` || currentPath === `/document/${doc.id}/edit`) {
window.location.href = `/category/${category.id}`;
}
})
.catch(error => {
console.error('Error:', error);
showNotification('Error deleting document', 'error');
});
}
});
docActions.appendChild(deleteBtn);
const docActions = createDocumentActions(doc, docLi, category.id);
docLi.appendChild(docLink);
docLi.appendChild(docActions);