763 lines
No EOL
38 KiB
HTML
763 lines
No EOL
38 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<meta name="csrf-token" content="{{ csrf_token() }}">
|
|
<title>{% block title %}Vim Docs{% endblock %}</title>
|
|
|
|
<!-- Material Design Icons -->
|
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/font@6.9.96/css/materialdesignicons.min.css">
|
|
|
|
<!-- Main CSS -->
|
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/main.css') }}">
|
|
|
|
<!-- CascadyaCove Nerd Font -->
|
|
<style>
|
|
@font-face {
|
|
font-family: 'CascadyaCove NF';
|
|
src: url('/static/fonts/CascadiaCove.ttf') format('truetype');
|
|
font-weight: normal;
|
|
font-style: normal;
|
|
}
|
|
</style>
|
|
|
|
<!-- Tailwind CSS -->
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<script>
|
|
tailwind.config = {
|
|
darkMode: 'class',
|
|
theme: {
|
|
extend: {
|
|
colors: {
|
|
primary: {% if current_user.is_authenticated %}'{{ current_user.theme_color }}'{% else %}'#50fa7b'{% endif %},
|
|
'primary-dark': {% if current_user.is_authenticated %}'{{ current_user.theme_color }}'{% else %}'#39bd5e'{% endif %},
|
|
'primary-light': {% if current_user.is_authenticated %}'{{ current_user.theme_color }}'{% else %}'#7dfb96'{% endif %},
|
|
'gray': {
|
|
800: '#282a36',
|
|
900: '#1e1f29',
|
|
}
|
|
},
|
|
fontFamily: {
|
|
mono: ['CascadyaCove NF', 'monospace']
|
|
}
|
|
}
|
|
}
|
|
};
|
|
</script>
|
|
|
|
<!-- Custom Styles -->
|
|
<!-- All styles moved to main.css -->
|
|
|
|
{% block extra_css %}{% endblock %}
|
|
</head>
|
|
<body class="bg-gray-900 text-gray-300 min-h-screen font-sans">
|
|
{% if current_user.is_authenticated %}
|
|
<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">
|
|
<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">
|
|
<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' }}">
|
|
<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">
|
|
<!-- 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">
|
|
<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">
|
|
<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' }}">
|
|
<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">
|
|
<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
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
</nav>
|
|
</aside>
|
|
|
|
<!-- Main content -->
|
|
<main class="flex-1 flex flex-col ml-64 transition-all ease-in-out duration-300">
|
|
<!-- Header -->
|
|
<header class="bg-gray-800 border-b border-gray-700 p-4 flex justify-between items-center sticky top-0 z-10">
|
|
<div class="flex items-center">
|
|
<button id="toggle-sidebar" class="p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 transition-colors mr-3">
|
|
<i class="mdi mdi-menu"></i>
|
|
</button>
|
|
<h2 class="text-white text-lg font-medium">{% block header_title %}Dashboard{% endblock %}</h2>
|
|
|
|
<div class="relative ml-4">
|
|
<div class="flex items-center h-9 rounded-md border border-gray-700 bg-gray-900">
|
|
<i class="mdi mdi-magnify text-gray-400 mx-3"></i>
|
|
<input type="text" id="search-input"
|
|
class="bg-transparent border-0 outline-none text-white w-64"
|
|
placeholder="Search documents...">
|
|
</div>
|
|
<div id="search-results" class="absolute left-0 right-0 top-full mt-1 bg-gray-800 rounded-md shadow-lg z-20 max-h-96 overflow-y-auto hidden"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex items-center space-x-2">
|
|
{% block header_actions %}{% endblock %}
|
|
|
|
<div class="relative ml-2">
|
|
<button id="user-menu-button" class="p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 transition-colors flex items-center">
|
|
<span class="w-8 h-8 bg-primary/20 rounded-full flex items-center justify-center text-primary mr-2">
|
|
{{ current_user.username[0].upper() }}
|
|
</span>
|
|
<span class="hidden md:inline mr-1">{{ current_user.username }}</span>
|
|
<i class="mdi mdi-chevron-down text-sm"></i>
|
|
</button>
|
|
|
|
<div id="user-dropdown" class="absolute right-0 mt-2 py-2 w-48 bg-gray-800 rounded-md shadow-lg z-20 hidden">
|
|
<a href="{{ url_for('auth.settings') }}" class="{{ 'bg-gray-700' if request.endpoint == 'auth.settings' }} block px-4 py-2 text-gray-300 hover:bg-gray-700 hover:text-white w-full text-left">
|
|
<i class="mdi mdi-cog-outline mr-2"></i> Settings
|
|
</a>
|
|
<a href="{{ url_for('auth.logout') }}" class="block px-4 py-2 text-gray-300 hover:bg-gray-700 hover:text-white w-full text-left">
|
|
<i class="mdi mdi-logout-variant mr-2"></i> Sign Out
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- Content area -->
|
|
<div class="flex-1 p-6 overflow-auto">
|
|
{% block content %}{% endblock %}
|
|
</div>
|
|
</main>
|
|
</div>
|
|
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Toggle sidebar
|
|
const toggleBtn = document.getElementById('toggle-sidebar');
|
|
if (toggleBtn) {
|
|
toggleBtn.addEventListener('click', function() {
|
|
document.querySelector('body > div').classList.toggle('sidebar-hidden');
|
|
|
|
// Save preference in cookie
|
|
const isCollapsed = document.querySelector('body > div').classList.contains('sidebar-hidden');
|
|
document.cookie = `sidebar_collapsed=${isCollapsed}; path=/; max-age=31536000`;
|
|
});
|
|
}
|
|
|
|
// User dropdown
|
|
const userMenuBtn = document.getElementById('user-menu-button');
|
|
const userDropdown = document.getElementById('user-dropdown');
|
|
|
|
if (userMenuBtn && userDropdown) {
|
|
userMenuBtn.addEventListener('click', function(e) {
|
|
e.stopPropagation();
|
|
userDropdown.classList.toggle('hidden');
|
|
});
|
|
|
|
document.addEventListener('click', function(e) {
|
|
if (!userMenuBtn.contains(e.target) && !userDropdown.contains(e.target)) {
|
|
userDropdown.classList.add('hidden');
|
|
}
|
|
});
|
|
}
|
|
|
|
// Load categories
|
|
loadCategories();
|
|
|
|
// Search functionality
|
|
setupSearch();
|
|
});
|
|
|
|
function loadCategories() {
|
|
const categoryTree = document.getElementById('category-tree');
|
|
if (!categoryTree) return;
|
|
|
|
fetch('/api/categories')
|
|
.then(response => response.json())
|
|
.then(categories => {
|
|
categoryTree.innerHTML = ''; // Clear existing items
|
|
|
|
if (categories.length === 0) {
|
|
categoryTree.innerHTML = '<li class="px-4 py-2 text-gray-500">No categories found</li>';
|
|
return;
|
|
}
|
|
|
|
categories.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>';
|
|
});
|
|
}
|
|
|
|
function createCategoryItem(category) {
|
|
const li = document.createElement('li');
|
|
li.className = 'category-item my-1';
|
|
li.dataset.categoryId = category.id;
|
|
|
|
// Create the category item container with flexbox to place the + icon
|
|
const categoryContainer = document.createElement('div');
|
|
categoryContainer.className = 'flex items-center group relative';
|
|
li.appendChild(categoryContainer);
|
|
|
|
// Add toggle button for collapsing/expanding if has children
|
|
const hasChildren = category.children && category.children.length > 0;
|
|
const hasDocuments = category.documents && category.documents.length > 0;
|
|
const isExpandable = hasChildren || hasDocuments;
|
|
|
|
// Add category toggle/expand button
|
|
const toggleButton = document.createElement('button');
|
|
toggleButton.className = 'w-4 text-gray-500 hover:text-primary flex-shrink-0';
|
|
// Use simpler conditional to avoid linter issues with template strings in JS
|
|
if (isExpandable) {
|
|
toggleButton.innerHTML = '<i class="mdi mdi-chevron-right text-sm"></i>';
|
|
} else {
|
|
toggleButton.innerHTML = '<i class="mdi mdi-chevron-right text-sm opacity-0"></i>';
|
|
}
|
|
categoryContainer.appendChild(toggleButton);
|
|
|
|
// 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';
|
|
|
|
// Special styling for root
|
|
if (category.is_root) {
|
|
categoryClass += ' font-semibold text-primary';
|
|
}
|
|
|
|
a.className = categoryClass;
|
|
|
|
// Special icon for root
|
|
const iconClass = category.is_root ? 'mdi-folder-root' : category.icon;
|
|
a.innerHTML = `<i class="mdi ${iconClass} mr-2 text-sm"></i> <span class="truncate">${category.name}</span>`;
|
|
categoryContainer.appendChild(a);
|
|
|
|
// 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';
|
|
categoryContainer.appendChild(actionsContainer);
|
|
|
|
// Add document button - consistent across all views
|
|
const newDocButton = document.createElement('button');
|
|
newDocButton.className = 'p-1 text-gray-500 hover:text-primary rounded transition-colors';
|
|
newDocButton.title = 'Add document';
|
|
newDocButton.innerHTML = '<i class="mdi mdi-file-plus-outline text-sm"></i>';
|
|
newDocButton.addEventListener('click', function(e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
window.location.href = `/document/new?category=${category.id}`;
|
|
});
|
|
actionsContainer.appendChild(newDocButton);
|
|
|
|
// Add subcategory button - consistent across all views
|
|
const newSubfolderButton = document.createElement('button');
|
|
newSubfolderButton.className = 'p-1 text-gray-500 hover:text-primary rounded transition-colors';
|
|
newSubfolderButton.title = 'Add subcategory';
|
|
newSubfolderButton.innerHTML = '<i class="mdi mdi-folder-plus-outline text-sm"></i>';
|
|
newSubfolderButton.addEventListener('click', function(e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
window.location.href = `/category/new?parent_id=${category.id}`;
|
|
});
|
|
actionsContainer.appendChild(newSubfolderButton);
|
|
|
|
// Edit category button - consistent across all views
|
|
const editButton = document.createElement('button');
|
|
editButton.className = 'p-1 text-gray-500 hover:text-primary rounded transition-colors';
|
|
editButton.title = 'Rename category';
|
|
editButton.innerHTML = '<i class="mdi mdi-pencil-outline text-sm"></i>';
|
|
editButton.addEventListener('click', function(e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
window.location.href = `/category/${category.id}/edit`;
|
|
});
|
|
actionsContainer.appendChild(editButton);
|
|
|
|
// 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.style.display = 'none'; // Initially collapsed
|
|
li.appendChild(childrenContainer);
|
|
|
|
// Toggle button functionality
|
|
toggleButton.addEventListener('click', function(e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
|
|
// Toggle visibility
|
|
const isCollapsed = childrenContainer.style.display === 'none';
|
|
childrenContainer.style.display = isCollapsed ? 'block' : 'none';
|
|
|
|
// Update icon - simpler approach to avoid linter issues
|
|
if (isCollapsed) {
|
|
toggleButton.innerHTML = '<i class="mdi mdi-chevron-down text-sm"></i>';
|
|
} else {
|
|
toggleButton.innerHTML = '<i class="mdi mdi-chevron-right text-sm"></i>';
|
|
}
|
|
|
|
// Save the category expanded state in localStorage
|
|
const expandedCategories = JSON.parse(localStorage.getItem('expandedCategories') || '[]');
|
|
const categoryId = category.id.toString();
|
|
|
|
if (isCollapsed) {
|
|
// Add to expanded list if not already there
|
|
if (!expandedCategories.includes(categoryId)) {
|
|
expandedCategories.push(categoryId);
|
|
}
|
|
} else {
|
|
// Remove from expanded list
|
|
const index = expandedCategories.indexOf(categoryId);
|
|
if (index > -1) {
|
|
expandedCategories.splice(index, 1);
|
|
}
|
|
}
|
|
|
|
localStorage.setItem('expandedCategories', JSON.stringify(expandedCategories));
|
|
});
|
|
|
|
// Initialize expanded state from localStorage if needed
|
|
const expandedCategories = JSON.parse(localStorage.getItem('expandedCategories') || '[]');
|
|
if (expandedCategories.includes(category.id.toString())) {
|
|
childrenContainer.style.display = 'block';
|
|
toggleButton.innerHTML = '<i class="mdi mdi-chevron-down text-sm"></i>';
|
|
}
|
|
|
|
// If this category has documents or child categories, add them
|
|
if (isExpandable) {
|
|
// Add documents first
|
|
if (hasDocuments) {
|
|
const documentsUl = document.createElement('ul');
|
|
documentsUl.className = 'py-1 space-y-1';
|
|
childrenContainer.appendChild(documentsUl);
|
|
|
|
// Sort documents by name
|
|
const docs = [...category.documents];
|
|
|
|
// Add document items
|
|
docs.forEach(docId => {
|
|
// We need to fetch the document title since we only have IDs
|
|
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 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 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);
|
|
|
|
docLi.appendChild(docLink);
|
|
docLi.appendChild(docActions);
|
|
documentsUl.appendChild(docLi);
|
|
})
|
|
.catch(error => {
|
|
console.error(`Error fetching document ${docId}:`, error);
|
|
});
|
|
});
|
|
}
|
|
|
|
// Add child categories after documents
|
|
if (hasChildren) {
|
|
const categoriesUl = document.createElement('ul');
|
|
categoriesUl.className = 'space-y-1 mt-1';
|
|
childrenContainer.appendChild(categoriesUl);
|
|
|
|
category.children.forEach(child => {
|
|
categoriesUl.appendChild(createCategoryItem(child));
|
|
});
|
|
}
|
|
}
|
|
|
|
// Add drop capability to the category
|
|
li.addEventListener('dragover', function(e) {
|
|
e.preventDefault();
|
|
li.classList.add('drop-target');
|
|
});
|
|
|
|
li.addEventListener('dragleave', function() {
|
|
li.classList.remove('drop-target');
|
|
});
|
|
|
|
li.addEventListener('drop', function(e) {
|
|
e.preventDefault();
|
|
li.classList.remove('drop-target');
|
|
|
|
try {
|
|
const data = JSON.parse(e.dataTransfer.getData('text/plain'));
|
|
|
|
if (data.type === 'document') {
|
|
// Handle document drop - move to this category
|
|
fetch(`/api/document/${data.id}`, {
|
|
method: 'PATCH',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRFToken': getCsrfToken()
|
|
},
|
|
body: JSON.stringify({
|
|
category_id: category.id
|
|
})
|
|
})
|
|
.then(response => {
|
|
if (response.ok) {
|
|
// Show success notification
|
|
showNotification(`Moved "${data.title}" to ${category.name}`);
|
|
// Refresh the category tree
|
|
loadCategories();
|
|
}
|
|
})
|
|
.catch(error => console.error('Error moving document:', error));
|
|
}
|
|
} catch (error) {
|
|
console.error('Error processing drop:', error);
|
|
}
|
|
});
|
|
|
|
return li;
|
|
}
|
|
|
|
// Helper function to show notifications
|
|
function showNotification(message, type = 'success') {
|
|
const notification = document.createElement('div');
|
|
const bgColor = type === 'success' ? 'bg-green-600' : 'bg-red-600';
|
|
|
|
notification.className = `fixed bottom-4 right-4 ${bgColor} text-white px-4 py-2 rounded-md shadow-lg transform translate-y-0 opacity-100 transition-all duration-300 z-50`;
|
|
notification.textContent = message;
|
|
|
|
document.body.appendChild(notification);
|
|
|
|
setTimeout(() => {
|
|
notification.classList.add('translate-y-16', 'opacity-0');
|
|
setTimeout(() => notification.remove(), 300);
|
|
}, 3000);
|
|
}
|
|
|
|
// Helper function to get CSRF token
|
|
function getCsrfToken() {
|
|
return document.querySelector('meta[name="csrf-token"]').getAttribute('content');
|
|
}
|
|
|
|
function setupSearch() {
|
|
const searchInput = document.getElementById('search-input');
|
|
const searchResults = document.getElementById('search-results');
|
|
|
|
if (!searchInput || !searchResults) return;
|
|
|
|
// Debounce function
|
|
function debounce(func, wait) {
|
|
let timeout;
|
|
return function() {
|
|
const context = this;
|
|
const args = arguments;
|
|
clearTimeout(timeout);
|
|
timeout = setTimeout(() => func.apply(context, args), wait);
|
|
};
|
|
}
|
|
|
|
const performSearch = debounce(function() {
|
|
const query = searchInput.value.trim();
|
|
|
|
if (query.length < 2) {
|
|
searchResults.classList.add('hidden');
|
|
return;
|
|
}
|
|
|
|
fetch(`/api/search?q=${encodeURIComponent(query)}`)
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
searchResults.innerHTML = '';
|
|
|
|
if (data.results.length === 0) {
|
|
searchResults.innerHTML = '<div class="p-4 text-gray-400">No results found</div>';
|
|
} else {
|
|
data.results.forEach(result => {
|
|
const item = document.createElement('a');
|
|
item.href = `/document/${result.id}`;
|
|
item.className = 'block p-3 hover:bg-gray-700 border-b border-gray-700 last:border-0';
|
|
|
|
let tagsHtml = '';
|
|
if (result.tags && result.tags.length > 0) {
|
|
tagsHtml = '<div class="flex gap-1 mt-1">' +
|
|
result.tags.map(tag => `<span class="text-xs px-2 py-1 bg-primary/20 text-primary rounded">${tag}</span>`).join('') +
|
|
'</div>';
|
|
}
|
|
|
|
let matchHtml = '';
|
|
if (result.match) {
|
|
const regex = new RegExp(`(${query})`, 'gi');
|
|
matchHtml = `<div class="text-sm text-gray-400 mt-1">${result.match.replace(regex, '<span class="bg-primary/30 text-primary">$1</span>')}</div>`;
|
|
}
|
|
|
|
item.innerHTML = `
|
|
<div class="font-medium text-white">${result.title}</div>
|
|
<div class="text-xs text-gray-400 mt-1">${result.category || 'Uncategorized'}</div>
|
|
${tagsHtml}
|
|
${matchHtml}
|
|
`;
|
|
|
|
searchResults.appendChild(item);
|
|
});
|
|
}
|
|
|
|
searchResults.classList.remove('hidden');
|
|
})
|
|
.catch(error => {
|
|
console.error('Search error:', error);
|
|
searchResults.innerHTML = '<div class="p-4 text-red-400">Error performing search</div>';
|
|
searchResults.classList.remove('hidden');
|
|
});
|
|
}, 300);
|
|
|
|
searchInput.addEventListener('input', performSearch);
|
|
searchInput.addEventListener('focus', function() {
|
|
if (searchInput.value.trim().length >= 2) {
|
|
searchResults.classList.remove('hidden');
|
|
}
|
|
});
|
|
|
|
document.addEventListener('click', function(e) {
|
|
if (!searchInput.contains(e.target) && !searchResults.contains(e.target)) {
|
|
searchResults.classList.add('hidden');
|
|
}
|
|
});
|
|
}
|
|
</script>
|
|
|
|
{% else %}
|
|
<div class="bg-gray-900 min-h-screen">
|
|
{% block auth_content %}{% endblock %}
|
|
</div>
|
|
{% endif %}
|
|
|
|
<script src="{{ url_for('static', filename='js/utils.js') }}"></script>
|
|
{% block extra_js %}{% endblock %}
|
|
|
|
<!-- Add this modal to the bottom of the body, before the closing body tag -->
|
|
<div id="delete-category-modal" class="fixed inset-0 bg-black bg-opacity-40 flex items-center justify-center hidden z-50">
|
|
<div class="bg-white rounded-lg shadow-xl max-w-md w-full p-6">
|
|
<h3 class="text-lg font-medium text-slate-900 mb-4">Delete Category</h3>
|
|
<p class="text-slate-700 mb-2">Are you sure you want to delete "<span id="category-delete-name" class="font-medium"></span>"?</p>
|
|
|
|
<div class="mt-4 space-y-3">
|
|
<div class="flex items-center">
|
|
<input type="radio" id="preserve-documents" name="delete-option" value="preserve" class="mr-2" checked>
|
|
<label for="preserve-documents" class="text-sm text-slate-700">
|
|
Move documents and subcategories to parent category
|
|
</label>
|
|
</div>
|
|
<div class="flex items-center">
|
|
<input type="radio" id="delete-all" name="delete-option" value="delete" class="mr-2">
|
|
<label for="delete-all" class="text-sm text-slate-700">
|
|
Delete all documents and subcategories
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-6 flex justify-end space-x-3">
|
|
<button id="cancel-delete-category" class="px-4 py-2 bg-slate-100 text-slate-700 rounded-md hover:bg-slate-200">
|
|
Cancel
|
|
</button>
|
|
<button id="confirm-delete-category" class="px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-700">
|
|
Delete
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Add this JavaScript at the bottom of the file, before the closing body tag -->
|
|
<script>
|
|
// Category deletion functionality
|
|
const categoryDeleteModal = document.getElementById('delete-category-modal');
|
|
const categoryNameSpan = document.getElementById('category-delete-name');
|
|
const cancelDeleteCategoryBtn = document.getElementById('cancel-delete-category');
|
|
const confirmDeleteCategoryBtn = document.getElementById('confirm-delete-category');
|
|
let currentCategoryId = null;
|
|
|
|
// Show notification function
|
|
function showNotification(message, type = 'success') {
|
|
const notification = document.createElement('div');
|
|
notification.className = `fixed bottom-4 right-4 p-4 rounded-md shadow-lg z-50 ${
|
|
type === 'success' ? 'bg-green-500' : 'bg-red-500'
|
|
} text-white`;
|
|
notification.textContent = message;
|
|
document.body.appendChild(notification);
|
|
|
|
setTimeout(() => {
|
|
notification.remove();
|
|
}, 3000);
|
|
}
|
|
|
|
// Show delete confirmation modal
|
|
document.querySelectorAll('.delete-category-btn').forEach(btn => {
|
|
btn.addEventListener('click', function(e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
|
|
// Close any open dropdown
|
|
document.querySelectorAll('.dropdown-menu:not(.hidden)').forEach(menu => {
|
|
menu.classList.add('hidden');
|
|
});
|
|
|
|
const categoryId = this.getAttribute('data-category-id');
|
|
const categoryName = this.getAttribute('data-category-name');
|
|
|
|
categoryNameSpan.textContent = categoryName;
|
|
currentCategoryId = categoryId;
|
|
categoryDeleteModal.classList.remove('hidden');
|
|
});
|
|
});
|
|
|
|
// Cancel category deletion
|
|
cancelDeleteCategoryBtn.addEventListener('click', function() {
|
|
categoryDeleteModal.classList.add('hidden');
|
|
currentCategoryId = null;
|
|
});
|
|
|
|
// Confirm category deletion
|
|
confirmDeleteCategoryBtn.addEventListener('click', function() {
|
|
if (currentCategoryId) {
|
|
const preserveContents = document.getElementById('preserve-documents').checked;
|
|
|
|
// Send delete request to server
|
|
fetch(`/api/category/${currentCategoryId}?preserve_contents=${preserveContents}`, {
|
|
method: 'DELETE',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRFToken': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''
|
|
}
|
|
})
|
|
.then(response => {
|
|
if (!response.ok) {
|
|
throw new Error('Failed to delete category');
|
|
}
|
|
return response.json();
|
|
})
|
|
.then(data => {
|
|
// Close the modal
|
|
categoryDeleteModal.classList.add('hidden');
|
|
|
|
// Show a success message
|
|
showNotification('Category deleted successfully');
|
|
|
|
// Reload the page to refresh the category tree
|
|
setTimeout(() => {
|
|
window.location.reload();
|
|
}, 1000);
|
|
})
|
|
.catch(error => {
|
|
console.error('Error:', error);
|
|
showNotification('Error deleting category', 'error');
|
|
});
|
|
}
|
|
});
|
|
</script>
|
|
</body>
|
|
</html> |