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

@ -21,6 +21,108 @@ class Config:
PERMANENT_SESSION_LIFETIME = timedelta(hours=12)
SESSION_TYPE = 'filesystem'
def init_db(app):
"""Initialize the database and create tables if they don't exist."""
with app.app_context():
db.create_all()
from app.models.user import User
# Create a demo user if no users exist
if User.query.count() == 0:
from app.models.document import Document, Category, Tag
from werkzeug.security import generate_password_hash
print('Creating demo user...')
# Create demo user
demo_user = User(username='demo')
demo_user.set_password('password')
db.session.add(demo_user)
db.session.flush() # To get the user ID
# Create a root category for the demo user
root_category = Category(
name='My Documents',
icon='mdi-folder',
description='Default document category',
user_id=demo_user.id,
is_root=True
)
db.session.add(root_category)
# Create some sample categories
categories = [
Category(
name='Vim Commands',
icon='mdi-vim',
user_id=demo_user.id,
description='Essential Vim commands and shortcuts'
),
Category(
name='Flask Development',
icon='mdi-flask',
user_id=demo_user.id,
description='Flask web development notes'
),
Category(
name='Python Snippets',
icon='mdi-language-python',
user_id=demo_user.id,
description='Useful Python code snippets'
)
]
for category in categories:
db.session.add(category)
# Create a sample document
sample_doc = Document(
title='Getting Started with Vim',
content="""# Getting Started with Vim
## Basic Commands
### Movement
- `h` - move left
- `j` - move down
- `k` - move up
- `l` - move right
### Modes
- `i` - enter insert mode
- `Esc` - return to normal mode
- `v` - enter visual mode
- `:` - enter command mode
> Vim has a steep learning curve, but it's worth it!
> [!TIP]
> Use `vimtutor` to learn Vim basics interactively.
> [!NOTE]
> Vim is available on almost all Unix-like systems.
""",
user_id=demo_user.id,
category_id=categories[0].id
)
db.session.add(sample_doc)
# Create some tags
tags = [
Tag(name='vim', user_id=demo_user.id, color='#50fa7b'),
Tag(name='editor', user_id=demo_user.id, color='#bd93f9'),
Tag(name='tutorial', user_id=demo_user.id, color='#ff79c6')
]
for tag in tags:
db.session.add(tag)
# Associate tags with the document
sample_doc.tags = tags
# Commit all changes
db.session.commit()
print('Demo user and sample data created successfully!')
def create_app(config_class=Config):
app = Flask(__name__)
app.config.from_object(config_class)
@ -51,6 +153,9 @@ def create_app(config_class=Config):
# Create app instance
app = create_app()
# Initialize database
init_db(app)
# Import models after db initialization to avoid circular imports
from app.models.document import Document, Category, Tag
from app.models.user import User

View file

@ -45,9 +45,9 @@ def signup():
# Create root category for the user
root_category = Category(
name='Library',
icon='mdi-bookshelf',
description='General storage for documents and categories',
name='', # Empty name for the root
icon='mdi-folder-outline',
description='Default container for documents and categories',
user_id=user.id,
is_root=True
)

View file

@ -48,10 +48,18 @@ class Category(db.Model):
def __repr__(self):
return f'<Category {self.name}>'
@property
def display_name(self):
"""Return a display name for the category, showing 'Root' for the root category with empty name"""
if self.is_root and not self.name:
return "Root"
return self.name
def to_dict(self):
return {
'id': self.id,
'name': self.name,
'name': self.name or '', # Ensure name is never null
'display_name': self.display_name,
'icon': self.icon,
'description': self.description,
'parent_id': self.parent_id,

View file

@ -7,6 +7,7 @@ import json
from datetime import datetime
import io
from sqlalchemy import or_
import re
main = Blueprint('main', __name__)
@ -164,9 +165,9 @@ def save_document():
if not root_category:
# Create a root category if it doesn't exist
root_category = Category(
name='Library',
icon='mdi-bookshelf',
description='General storage for documents and categories',
name='',
icon='mdi-folder-outline',
description='Default container for documents and categories',
user_id=current_user.id,
is_root=True
)
@ -341,54 +342,64 @@ def delete_category(category_id):
return jsonify({'success': True})
@main.route('/api/search', methods=['GET'])
@login_required
@main.route('/search')
def search_documents():
"""Search for documents by title, content, or tags"""
query = request.args.get('q', '')
user_id = current_user.id
if not query or len(query) < 2:
return jsonify({'results': []})
if not query:
return jsonify([])
# Search in title, content, and tags for current user's documents only
docs = Document.query.filter(
Document.user_id == current_user.id,
or_(
Document.title.ilike(f'%{query}%'),
Document.content.ilike(f'%{query}%'),
Document.tags.any(Tag.name.ilike(f'%{query}%'))
)
).limit(10).all()
# Parse hashtags from the query
hashtags = re.findall(r'#(\w+)', query)
# Remove hashtags from the main query text
cleaned_query = re.sub(r'#\w+', '', query).strip()
# Base query for documents
base_query = Document.query.filter_by(user_id=user_id)
results = []
for doc in docs:
# Find match in content
match = None
if query.lower() in doc.content.lower():
# Find the sentence containing the match
content_lower = doc.content.lower()
query_pos = content_lower.find(query.lower())
# Get a snippet around the match
start = max(0, content_lower.rfind('.', 0, query_pos) + 1)
end = content_lower.find('.', query_pos)
if end == -1:
end = min(len(doc.content), query_pos + 200)
# If we have a cleaned query (non-hashtag part), search by title and content
if cleaned_query:
title_content_results = base_query.filter(
or_(
Document.title.ilike(f'%{cleaned_query}%'),
Document.content.ilike(f'%{cleaned_query}%')
)
).all()
results.extend(title_content_results)
match = doc.content[start:end].strip()
# If we have hashtags, search by tags (OR operation between tags)
if hashtags:
tag_results = base_query.join(document_tags).join(Tag).filter(
Tag.name.in_(hashtags)
).all()
# Get category name
category_name = doc.category.name if doc.category else None
# Add unique tag results to the results list
for doc in tag_results:
if doc not in results:
results.append(doc)
results.append({
# If no specific search criteria (cleaned query or hashtags), return empty list
if not cleaned_query and not hashtags:
return jsonify([])
# Convert to list of dictionaries for JSON response
docs_list = []
for doc in results:
doc_dict = {
'id': doc.id,
'title': doc.title,
'category': category_name,
'tags': [tag.name for tag in doc.tags],
'match': match
})
'preview': doc.content[:150] + '...' if len(doc.content) > 150 else doc.content,
'category_id': doc.category_id,
'created_at': doc.created_at.strftime('%Y-%m-%d %H:%M'),
'updated_at': doc.updated_at.strftime('%Y-%m-%d %H:%M') if doc.updated_at else None,
'tags': [tag.name for tag in doc.tags]
}
docs_list.append(doc_dict)
return jsonify({'results': results})
return jsonify(docs_list)
@main.route('/api/tags', methods=['GET'])
@login_required
@ -433,8 +444,10 @@ def new_category():
if parent_id:
parent = Category.query.filter_by(id=parent_id, user_id=current_user.id).first_or_404()
return render_template('category_edit.html', category=None, parent=parent)
return render_template('category_edit.html', category=None, parent=parent)
else:
# No parent specified, creating a top-level category
return render_template('category_edit.html', category=None, parent=None)
@main.route('/category/<int:category_id>/edit', methods=['GET'])
@login_required

View file

@ -35,8 +35,7 @@
/* Active view styling */
.active-view {
background-color: rgba(76, 175, 80, 0.2); /* Primary color with opacity */
color: #4CAF50; /* Primary color */
@apply bg-primary/20 text-primary;
}
/* Fix for sidebar hiding */
@ -78,6 +77,16 @@
z-index: 10;
}
/* Sidebar section headers */
.sidebar-section-header {
font-size: 0.75rem;
text-transform: uppercase;
color: rgba(156, 163, 175, 0.7);
letter-spacing: 0.05em;
padding: 0.5rem 1rem 0.25rem;
margin-top: 0.5rem;
}
/* Drag and drop styles */
.drop-target {
background-color: rgba(80, 250, 123, 0.15);
@ -173,3 +182,69 @@
.markdown-body .admonition-danger .admonition-title {
color: #cf222e;
}
/* Modern Sidebar Styling */
aside {
background-color: #1a1b26;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.2);
}
/* Reduce vertical spacing of sidebar items */
aside nav ul li {
margin: 0.15rem 0;
}
/* Better styling for sidebar links */
aside nav ul li a {
padding: 0.4rem 0.75rem;
border-radius: 0.25rem;
transition: all 0.15s ease;
}
aside nav ul li a:hover {
background-color: rgba(255, 255, 255, 0.05);
}
/* Remove the border-left styling in category trees */
aside nav ul.ml-3.pl-3.border-l.border-gray-700,
.ml-3.pl-3.border-l.border-gray-700,
.ml-2.pl-2.border-l.border-gray-700 {
margin-left: 1.25rem !important;
padding-left: 0 !important;
border-left: none !important;
}
/* Better nested document styling */
.document-item a,
.category-item a {
font-size: 0.875rem;
display: flex;
align-items: center;
padding: 0.35rem 0.5rem;
}
/* Cleaner category headers */
aside nav ul li div.block.font-medium {
padding: 0.4rem 0.75rem;
font-size: 0.8rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: rgba(255, 255, 255, 0.5);
margin-top: 1rem;
}
/* Make the toggle buttons more subtle */
.toggle-btn {
opacity: 0.6;
transition: all 0.15s ease;
}
.toggle-btn:hover {
opacity: 1;
}
/* Better styling for the primary action button */
aside nav ul li a.bg-primary {
margin-top: 0.5rem;
box-shadow: 0 3px 8px rgba(80, 250, 123, 0.2);
}

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);

View file

@ -1,21 +1,23 @@
{% extends "base.html" %}
{% block title %}{{ category.name }} - Vim Docs{% endblock %}
{% block title %}{{ category.display_name }} - Vim Docs{% endblock %}
{% block header_title %}
<i class="mdi {{ category.icon }} mr-2"></i> {{ category.name }}
<i class="mdi {{ category.icon }} mr-2"></i> {{ category.display_name }}
{% endblock %}
{% block header_actions %}
<a href="{{ url_for('main.new_document') }}?category={{ category.id }}" class="inline-flex items-center px-4 py-2 bg-primary text-black rounded-md hover:bg-primary-dark transition-colors">
<i class="mdi mdi-plus mr-2"></i> New Document
</a>
<a href="{{ url_for('main.new_category') }}?parent_id={{ category.id }}" class="inline-flex items-center px-4 py-2 bg-primary/80 text-black rounded-md hover:bg-primary-dark transition-colors ml-2">
<i class="mdi mdi-folder-plus-outline mr-2"></i> New Subcategory
<a href="{{ url_for('main.new_category') }}{% if not category.is_root %}?parent_id={{ category.id }}{% endif %}" class="inline-flex items-center px-4 py-2 bg-primary/80 text-black rounded-md hover:bg-primary-dark transition-colors ml-2">
<i class="mdi mdi-folder-plus-outline mr-2"></i> {% if category.is_root %}New Category{% else %}New Subcategory{% endif %}
</a>
{% if not category.is_root %}
<a href="{{ url_for('main.edit_category', category_id=category.id) }}" class="inline-flex items-center px-4 py-2 bg-gray-700 text-white rounded-md hover:bg-gray-600 transition-colors ml-2">
<i class="mdi mdi-pencil mr-2"></i> Edit Category
</a>
{% endif %}
<a href="{{ url_for('main.category_all_documents', category_id=category.id) }}" class="inline-flex items-center px-4 py-2 bg-gray-700 text-white rounded-md hover:bg-gray-600 transition-colors ml-2">
<i class="mdi mdi-file-document-multiple-outline mr-2"></i> All Documents
</a>
@ -29,14 +31,18 @@
<div class="w-10 h-10 rounded-md bg-primary/20 flex items-center justify-center text-primary mr-3">
<i class="mdi {{ category.icon }} text-2xl"></i>
</div>
<h1 class="text-2xl font-bold text-white">{{ category.name }}</h1>
<h1 class="text-2xl font-bold text-white">{{ category.display_name }}</h1>
</div>
<div class="text-gray-400 mb-4">
{% if category.description %}
{{ category.description }}
{% else %}
{% if category.is_root %}
The default container for all your documents and categories
{% else %}
A category for organizing your documents
{% endif %}
{% endif %}
</div>
@ -46,10 +52,12 @@
<a href="{{ url_for('main.index') }}" class="hover:text-primary">Home</a>
{% if category.parent %}
<span class="mx-2">/</span>
<a href="{{ url_for('main.view_category', category_id=category.parent.id) }}" class="hover:text-primary">{{ category.parent.name }}</a>
<a href="{{ url_for('main.view_category', category_id=category.parent.id) }}" class="hover:text-primary">{{ category.parent.display_name }}</a>
{% endif %}
{% if not category.is_root %}
<span class="mx-2">/</span>
<span class="text-gray-400">{{ category.name }}</span>
<span class="text-gray-400">{{ category.display_name }}</span>
{% endif %}
</div>
</div>
</div>
@ -72,7 +80,7 @@
<div class="w-10 h-10 rounded-md bg-primary/20 flex items-center justify-center text-primary mr-3">
<i class="mdi {{ subcategory.icon }} text-2xl"></i>
</div>
<h3 class="text-white font-medium truncate">{{ subcategory.name }}</h3>
<h3 class="text-white font-medium truncate">{{ subcategory.display_name }}</h3>
</div>
{% if subcategory.description %}
@ -94,7 +102,7 @@
<i class="mdi mdi-folder-plus-outline text-2xl text-gray-500"></i>
</div>
<h3 class="text-gray-400 font-medium mb-1">New Subcategory</h3>
<p class="text-gray-500 text-sm">Add a subcategory to {{ category.name }}</p>
<p class="text-gray-500 text-sm">Add a subcategory to {{ category.display_name }}</p>
</a>
</div>
</div>

View file

@ -8,9 +8,15 @@
<button id="save-button" class="inline-flex items-center px-4 py-2 bg-primary text-black rounded-md hover:bg-primary-dark transition-colors">
<i class="mdi mdi-content-save mr-2"></i> Save
</button>
{% if category %}
<a href="{{ url_for('main.view_category', category_id=category.id) }}" class="inline-flex items-center px-4 py-2 bg-gray-700 text-white rounded-md hover:bg-gray-600 transition-colors ml-2">
<i class="mdi mdi-arrow-left mr-2"></i> Back
</a>
{% else %}
<a href="{{ url_for('main.index') }}" class="inline-flex items-center px-4 py-2 bg-gray-700 text-white rounded-md hover:bg-gray-600 transition-colors ml-2">
<i class="mdi mdi-arrow-left mr-2"></i> Back
</a>
{% endif %}
{% if category and not category.is_root %}
<button id="delete-category-btn" class="inline-flex items-center px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-500 transition-colors ml-2">
<i class="mdi mdi-delete mr-2"></i> Delete
@ -91,7 +97,7 @@
<label for="category-parent" class="block text-sm font-medium text-gray-400 mb-1">Parent Category</label>
<select id="category-parent"
class="w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded-md text-white focus:outline-none focus:ring-2 focus:ring-primary focus:border-primary">
<option value="">None (Root Category)</option>
<option value="">No Parent</option>
<!-- Options will be populated with JavaScript -->
</select>
</div>
@ -202,13 +208,13 @@
<div class="p-6">
<p class="text-gray-300 mb-6">
Are you sure you want to delete <span class="font-semibold text-white">{{ category.name if category else 'this category' }}</span>?
{% if category and (category.documents.count() > 0 or category.children.count() > 0) %}
Are you sure you want to delete <span class="font-semibold text-white">{{ category.name if category is not none else 'this category' }}</span>?
{% if category is not none and (category.documents.count() > 0 or category.children.count() > 0) %}
This category contains {{ category.documents.count() }} document(s) and {{ category.children.count() }} subcategory(ies).
{% endif %}
</p>
{% if category and (category.documents.count() > 0 or category.children.count() > 0) %}
{% if category is not none and (category.documents.count() > 0 or category.children.count() > 0) %}
<div class="mb-6">
<p class="text-white mb-2">What should happen to the contents?</p>
<div class="space-y-3">
@ -266,15 +272,15 @@
fetch('/api/categories')
.then(response => response.json())
.then(categories => {
// Find the root category
const rootCategory = categories.find(c => c.name === 'root');
// Add options recursively
function addCategoryOptions(categories, depth = 0) {
categories.forEach(category => {
// Skip the category being edited to avoid circular references
{% if category %}
if (category.id === {{ category.id }}) return;
// Also skip any root categories as they shouldn't be selectable as parents
{% if category is not none %}
if (category.id === {{ category.id }} || category.is_root) return;
{% else %}
if (category.is_root) return;
{% endif %}
const option = document.createElement('option');
@ -287,23 +293,16 @@
// Select option logic:
// 1. If we have a parent specified, select that parent
// 2. If we're editing an existing category, select its current parent
// 3. If creating a new category, select root by default
let selectThisOption = false;
{% if parent %}
{% if parent is not none %}
if (category.id === {{ parent.id }}) {
selectThisOption = true;
}
{% elif category and category.parent_id %}
{% elif category is not none and category.parent_id is not none %}
if (category.id === {{ category.parent_id }}) {
selectThisOption = true;
}
{% else %}
// If no parent specified and creating new category, default to root
if (category.is_root && !rootSelected) {
selectThisOption = true;
rootSelected = true;
}
{% endif %}
option.selected = selectThisOption;
@ -315,10 +314,7 @@
});
}
// Initialize flag to track if root has been selected
let rootSelected = false;
// Start from root categories
// Start from categories
addCategoryOptions(categories);
})
.catch(error => {
@ -538,7 +534,8 @@
const queryParams = deleteOption === 'delete' ? '' : '?preserve_contents=true';
// Send delete request
fetch(`/api/category/{{ category.id if category else '' }}${queryParams}`, {
{% if category is not none %}
fetch(`/api/category/{{ category.id }}${queryParams}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
@ -568,7 +565,7 @@
// Redirect to home page or parent category
setTimeout(() => {
{% if category and category.parent_id %}
{% if category.parent_id %}
window.location.href = '/category/{{ category.parent_id }}';
{% else %}
window.location.href = '/';
@ -590,6 +587,7 @@
setTimeout(() => notification.remove(), 300);
}, 3000);
});
{% endif %}
});
}
});

View file

@ -535,6 +535,12 @@
const listView = document.getElementById('list-view');
if (gridViewBtn && listViewBtn) {
// Initialize view (grid is default)
gridViewBtn.classList.add('active-view');
listViewBtn.classList.remove('active-view');
gridView.classList.remove('hidden');
listView.classList.add('hidden');
// Load saved preference
const savedView = localStorage.getItem('categoryViewPreference');
if (savedView === 'list') {