wip
This commit is contained in:
parent
17885b005c
commit
f5c8e9ee23
4 changed files with 756 additions and 44 deletions
|
@ -296,28 +296,49 @@ def delete_category(category_id):
|
||||||
if category.is_root:
|
if category.is_root:
|
||||||
return jsonify({'error': 'Cannot delete root category'}), 400
|
return jsonify({'error': 'Cannot delete root category'}), 400
|
||||||
|
|
||||||
# Get target category for documents if specified
|
# Check if we should preserve contents
|
||||||
new_category_id = request.args.get('new_category_id')
|
preserve_contents = request.args.get('preserve_contents', 'false').lower() == 'true'
|
||||||
if new_category_id:
|
|
||||||
new_category = Category.query.filter_by(id=new_category_id, user_id=current_user.id).first()
|
# Find parent category or root
|
||||||
if new_category:
|
parent_category = None
|
||||||
# Reassign documents
|
if category.parent_id:
|
||||||
for doc in category.documents:
|
parent_category = Category.query.filter_by(id=category.parent_id, user_id=current_user.id).first()
|
||||||
doc.category_id = new_category.id
|
|
||||||
else:
|
if not parent_category:
|
||||||
# Move documents to no category
|
# If no parent or parent not found, use root category
|
||||||
|
parent_category = Category.query.filter_by(user_id=current_user.id, is_root=True).first()
|
||||||
|
|
||||||
|
if preserve_contents:
|
||||||
|
# Move documents to parent category
|
||||||
for doc in category.documents:
|
for doc in category.documents:
|
||||||
doc.category_id = None
|
doc.category_id = parent_category.id
|
||||||
|
|
||||||
# Also handle child categories
|
# Move child categories to parent
|
||||||
for child in category.children:
|
for child in category.children:
|
||||||
if new_category_id:
|
child.parent_id = parent_category.id
|
||||||
child.parent_id = new_category_id
|
else:
|
||||||
else:
|
# Delete all documents in this category
|
||||||
child.parent_id = None
|
for doc in category.documents:
|
||||||
|
db.session.delete(doc)
|
||||||
|
|
||||||
|
# Recursively delete subcategories and their documents
|
||||||
|
def delete_subcategories(parent):
|
||||||
|
for child in parent.children:
|
||||||
|
# Delete all documents in this subcategory
|
||||||
|
for doc in child.documents:
|
||||||
|
db.session.delete(doc)
|
||||||
|
# Recursively delete subcategories
|
||||||
|
delete_subcategories(child)
|
||||||
|
# Delete the subcategory itself
|
||||||
|
db.session.delete(child)
|
||||||
|
|
||||||
|
# Start recursive deletion
|
||||||
|
delete_subcategories(category)
|
||||||
|
|
||||||
|
# Delete the category itself
|
||||||
db.session.delete(category)
|
db.session.delete(category)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
return jsonify({'success': True})
|
return jsonify({'success': True})
|
||||||
|
|
||||||
@main.route('/api/search', methods=['GET'])
|
@main.route('/api/search', methods=['GET'])
|
||||||
|
|
|
@ -397,23 +397,71 @@
|
||||||
const docActions = document.createElement('div');
|
const docActions = document.createElement('div');
|
||||||
docActions.className = 'actions absolute right-0 hidden group-hover:flex items-center bg-gray-800 px-1';
|
docActions.className = 'actions absolute right-0 hidden group-hover:flex items-center bg-gray-800 px-1';
|
||||||
|
|
||||||
// Edit doc button
|
// Edit document button
|
||||||
const editDocButton = document.createElement('button');
|
const editBtn = document.createElement('button');
|
||||||
editDocButton.className = 'p-1 text-gray-500 hover:text-primary rounded transition-colors';
|
editBtn.className = 'p-1 text-gray-500 hover:text-primary rounded transition-colors';
|
||||||
editDocButton.title = 'Edit Document';
|
editBtn.title = 'Edit document';
|
||||||
editDocButton.innerHTML = '<i class="mdi mdi-pencil-outline text-sm"></i>';
|
editBtn.innerHTML = '<i class="mdi mdi-pencil-outline text-sm"></i>';
|
||||||
editDocButton.addEventListener('click', function(e) {
|
editBtn.addEventListener('click', function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
window.location.href = `/document/${doc.id}/edit`;
|
window.location.href = `/document/${doc.id}/edit`;
|
||||||
});
|
});
|
||||||
docActions.appendChild(editDocButton);
|
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(docLink);
|
||||||
docLi.appendChild(docActions);
|
docLi.appendChild(docActions);
|
||||||
documentsUl.appendChild(docLi);
|
documentsUl.appendChild(docLi);
|
||||||
})
|
})
|
||||||
.catch(error => console.error('Error fetching document:', error));
|
.catch(error => {
|
||||||
|
console.error(`Error fetching document ${docId}:`, error);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -477,10 +525,12 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to show notifications
|
// Helper function to show notifications
|
||||||
function showNotification(message) {
|
function showNotification(message, type = 'success') {
|
||||||
const notification = document.createElement('div');
|
const notification = document.createElement('div');
|
||||||
notification.className = 'fixed bottom-4 right-4 bg-primary/90 text-white px-4 py-2 rounded-md shadow-lg transform translate-y-0 opacity-100 transition-all duration-300 flex items-center';
|
const bgColor = type === 'success' ? 'bg-green-600' : 'bg-red-600';
|
||||||
notification.innerHTML = `<i class="mdi mdi-check-circle mr-2"></i><span>${message}</span>`;
|
|
||||||
|
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);
|
document.body.appendChild(notification);
|
||||||
|
|
||||||
|
@ -490,7 +540,7 @@
|
||||||
}, 3000);
|
}, 3000);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to get CSRF token from meta tag
|
// Helper function to get CSRF token
|
||||||
function getCsrfToken() {
|
function getCsrfToken() {
|
||||||
return document.querySelector('meta[name="csrf-token"]').getAttribute('content');
|
return document.querySelector('meta[name="csrf-token"]').getAttribute('content');
|
||||||
}
|
}
|
||||||
|
@ -589,5 +639,125 @@
|
||||||
|
|
||||||
<script src="{{ url_for('static', filename='js/utils.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/utils.js') }}"></script>
|
||||||
{% block extra_js %}{% endblock %}
|
{% 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>
|
</body>
|
||||||
</html>
|
</html>
|
|
@ -51,6 +51,104 @@
|
||||||
-webkit-line-clamp: 3;
|
-webkit-line-clamp: 3;
|
||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
transition: max-height 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.document-preview.expanded {
|
||||||
|
max-height: 20rem;
|
||||||
|
-webkit-line-clamp: unset;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.document-preview.fully-expanded {
|
||||||
|
max-height: 60rem;
|
||||||
|
-webkit-line-clamp: unset;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-fold-btn {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0.25rem;
|
||||||
|
color: #58a6ff;
|
||||||
|
background-color: rgba(88, 166, 255, 0.05);
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-fold-btn:hover {
|
||||||
|
background-color: rgba(88, 166, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.document-preview .markdown-content {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.document-preview .markdown-content h1 {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.document-preview .markdown-content h2 {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.document-preview .markdown-content h3,
|
||||||
|
.document-preview .markdown-content h4,
|
||||||
|
.document-preview .markdown-content h5,
|
||||||
|
.document-preview .markdown-content h6 {
|
||||||
|
font-size: 1rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.document-preview .markdown-content p {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.document-preview .markdown-content ul,
|
||||||
|
.document-preview .markdown-content ol {
|
||||||
|
padding-left: 1.5rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.document-preview .markdown-content pre {
|
||||||
|
background-color: #1e1e2e;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
overflow-x: auto;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.document-preview .markdown-content blockquote {
|
||||||
|
border-left: 3px solid #30363d;
|
||||||
|
padding-left: 0.5rem;
|
||||||
|
color: #8b949e;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.document-preview .markdown-content img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.document-preview .markdown-content table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.document-preview .markdown-content th,
|
||||||
|
.document-preview .markdown-content td {
|
||||||
|
border: 1px solid #30363d;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.document-tag {
|
.document-tag {
|
||||||
|
@ -91,6 +189,49 @@
|
||||||
background-color: rgba(80, 250, 123, 0.2);
|
background-color: rgba(80, 250, 123, 0.2);
|
||||||
color: #50fa7b;
|
color: #50fa7b;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.document-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn {
|
||||||
|
padding: 0.25rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
color: #8b949e;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn.delete:hover {
|
||||||
|
color: #f85149;
|
||||||
|
background-color: rgba(248, 81, 73, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-delete-modal {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-delete-content {
|
||||||
|
background-color: #2d333b;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
width: 90%;
|
||||||
|
max-width: 28rem;
|
||||||
|
padding: 1.5rem;
|
||||||
|
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
@ -108,7 +249,7 @@
|
||||||
|
|
||||||
<!-- View toggle -->
|
<!-- View toggle -->
|
||||||
<div class="view-toggle">
|
<div class="view-toggle">
|
||||||
<button id="grid-view-btn" class="view-toggle-btn active">
|
<button id="grid-view-btn" class="view-toggle-btn active-view">
|
||||||
<i class="mdi mdi-view-grid"></i> Grid
|
<i class="mdi mdi-view-grid"></i> Grid
|
||||||
</button>
|
</button>
|
||||||
<button id="list-view-btn" class="view-toggle-btn">
|
<button id="list-view-btn" class="view-toggle-btn">
|
||||||
|
@ -144,6 +285,9 @@
|
||||||
<a href="{{ url_for('main.export_document', doc_id=doc.id) }}" class="block px-4 py-2 text-gray-300 hover:bg-gray-600 hover:text-white">
|
<a href="{{ url_for('main.export_document', doc_id=doc.id) }}" class="block px-4 py-2 text-gray-300 hover:bg-gray-600 hover:text-white">
|
||||||
<i class="mdi mdi-download-outline mr-2"></i> Export
|
<i class="mdi mdi-download-outline mr-2"></i> Export
|
||||||
</a>
|
</a>
|
||||||
|
<button class="delete-document-btn block w-full text-left px-4 py-2 text-red-400 hover:bg-gray-600 hover:text-red-300" data-doc-id="{{ doc.id }}" data-doc-title="{{ doc.title }}">
|
||||||
|
<i class="mdi mdi-delete-outline mr-2"></i> Delete
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -158,9 +302,13 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="document-preview text-gray-400 text-sm mb-3">
|
<div class="document-preview text-gray-400 text-sm mb-3">
|
||||||
{{ doc.content[:200] }}{% if doc.content|length > 200 %}...{% endif %}
|
<div class="markdown-content" data-content="{{ doc.content }}">
|
||||||
|
<!-- Markdown will be rendered here -->
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<button class="preview-fold-btn" data-doc-id="{{ doc.id }}">Show more</button>
|
||||||
|
|
||||||
<div class="flex items-center justify-between mt-3 text-xs text-gray-500">
|
<div class="flex items-center justify-between mt-3 text-xs text-gray-500">
|
||||||
<div>
|
<div>
|
||||||
<i class="mdi mdi-clock-outline mr-1"></i>
|
<i class="mdi mdi-clock-outline mr-1"></i>
|
||||||
|
@ -220,20 +368,29 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="document-preview text-gray-400 text-sm">
|
<div class="document-preview text-gray-400 text-sm">
|
||||||
{{ doc.content[:200] }}{% if doc.content|length > 200 %}...{% endif %}
|
<div class="markdown-content" data-content="{{ doc.content }}">
|
||||||
|
<!-- Markdown will be rendered here -->
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<button class="preview-fold-btn" data-doc-id="{{ doc.id }}-list">Show more</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center mt-2 sm:mt-0">
|
<div class="flex items-center mt-2 sm:mt-0">
|
||||||
<a href="{{ url_for('main.view_document', doc_id=doc.id) }}" class="p-2 text-gray-400 hover:text-primary rounded" title="View">
|
<div class="document-actions">
|
||||||
<i class="mdi mdi-eye-outline"></i>
|
<a href="{{ url_for('main.view_document', doc_id=doc.id) }}" class="action-btn" title="View">
|
||||||
</a>
|
<i class="mdi mdi-eye-outline"></i>
|
||||||
<a href="{{ url_for('main.edit_document', doc_id=doc.id) }}" class="p-2 text-gray-400 hover:text-primary rounded" title="Edit">
|
</a>
|
||||||
<i class="mdi mdi-pencil-outline"></i>
|
<a href="{{ url_for('main.edit_document', doc_id=doc.id) }}" class="action-btn" title="Edit">
|
||||||
</a>
|
<i class="mdi mdi-pencil-outline"></i>
|
||||||
<a href="{{ url_for('main.export_document', doc_id=doc.id) }}" class="p-2 text-gray-400 hover:text-primary rounded" title="Export">
|
</a>
|
||||||
<i class="mdi mdi-download-outline"></i>
|
<a href="{{ url_for('main.export_document', doc_id=doc.id) }}" class="action-btn" title="Export">
|
||||||
</a>
|
<i class="mdi mdi-download-outline"></i>
|
||||||
|
</a>
|
||||||
|
<button class="action-btn delete delete-document-btn" title="Delete" data-doc-id="{{ doc.id }}" data-doc-title="{{ doc.title }}">
|
||||||
|
<i class="mdi mdi-delete-outline"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -259,11 +416,130 @@
|
||||||
<p class="text-gray-500">Try a different search term</p>
|
<p class="text-gray-500">Try a different search term</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Delete confirmation modal -->
|
||||||
|
<div id="delete-document-modal" class="confirm-delete-modal hidden">
|
||||||
|
<div class="confirm-delete-content">
|
||||||
|
<h3 class="text-lg font-medium text-white mb-4">Delete Document</h3>
|
||||||
|
<p class="text-gray-300 mb-6">Are you sure you want to delete "<span id="document-delete-name"></span>"? This action cannot be undone.</p>
|
||||||
|
<div class="flex justify-end space-x-3">
|
||||||
|
<button id="cancel-delete-doc" class="px-4 py-2 bg-gray-700 text-white rounded-md hover:bg-gray-600 transition-colors">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button id="confirm-delete-doc" class="px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-500 transition-colors">
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extra_js %}
|
{% block extra_js %}
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/highlight.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Set up Marked.js for markdown rendering
|
||||||
|
marked.setOptions({
|
||||||
|
gfm: true,
|
||||||
|
breaks: true,
|
||||||
|
sanitize: false,
|
||||||
|
highlight: function(code, lang) {
|
||||||
|
if (lang && hljs.getLanguage(lang)) {
|
||||||
|
return hljs.highlight(code, { language: lang }).value;
|
||||||
|
}
|
||||||
|
return hljs.highlightAuto(code).value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Custom renderer for alert blocks
|
||||||
|
const renderer = new marked.Renderer();
|
||||||
|
|
||||||
|
// Fix blockquote renderer for GitHub-style alerts
|
||||||
|
const originalBlockquote = renderer.blockquote;
|
||||||
|
renderer.blockquote = function(quote) {
|
||||||
|
const text = quote.replace(/<\/?p>/g, '');
|
||||||
|
const admonitionRegex = /^\s*\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION|DANGER)\]\s*([\s\S]*)/i;
|
||||||
|
const match = text.match(admonitionRegex);
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
const type = match[1].toLowerCase();
|
||||||
|
const title = match[1].charAt(0).toUpperCase() + match[1].slice(1).toLowerCase();
|
||||||
|
const content = match[2] ? match[2].trim() : '';
|
||||||
|
|
||||||
|
return `<div class="admonition admonition-${type}">
|
||||||
|
<p class="admonition-title">${title}</p>
|
||||||
|
<p>${content}</p>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return originalBlockquote.call(this, quote);
|
||||||
|
};
|
||||||
|
|
||||||
|
marked.use({ renderer });
|
||||||
|
|
||||||
|
// Render markdown content in document previews
|
||||||
|
document.querySelectorAll('.markdown-content').forEach(container => {
|
||||||
|
const content = container.getAttribute('data-content');
|
||||||
|
if (content) {
|
||||||
|
// Limit the content length for initial display
|
||||||
|
const previewLength = 300;
|
||||||
|
const shortenedContent = content.length > previewLength
|
||||||
|
? content.substring(0, previewLength) + "..."
|
||||||
|
: content;
|
||||||
|
|
||||||
|
container.innerHTML = marked.parse(shortenedContent);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle preview fold/unfold
|
||||||
|
const previewBtns = document.querySelectorAll('.preview-fold-btn');
|
||||||
|
previewBtns.forEach(btn => {
|
||||||
|
btn.addEventListener('click', function() {
|
||||||
|
const docId = this.getAttribute('data-doc-id');
|
||||||
|
const previewEl = this.previousElementSibling;
|
||||||
|
const markdownContainer = previewEl.querySelector('.markdown-content');
|
||||||
|
const content = markdownContainer.getAttribute('data-content');
|
||||||
|
|
||||||
|
if (previewEl.classList.contains('expanded')) {
|
||||||
|
if (previewEl.classList.contains('fully-expanded')) {
|
||||||
|
// Collapse to normal preview
|
||||||
|
previewEl.classList.remove('fully-expanded');
|
||||||
|
previewEl.classList.remove('expanded');
|
||||||
|
|
||||||
|
const previewLength = 300;
|
||||||
|
const shortenedContent = content.length > previewLength
|
||||||
|
? content.substring(0, previewLength) + "..."
|
||||||
|
: content;
|
||||||
|
|
||||||
|
markdownContainer.innerHTML = marked.parse(shortenedContent);
|
||||||
|
this.textContent = 'Show more';
|
||||||
|
} else {
|
||||||
|
// Expand to full view
|
||||||
|
previewEl.classList.add('fully-expanded');
|
||||||
|
markdownContainer.innerHTML = marked.parse(content);
|
||||||
|
this.textContent = 'Show less';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Expand to first level
|
||||||
|
previewEl.classList.add('expanded');
|
||||||
|
|
||||||
|
const previewLength = 1000;
|
||||||
|
const shortenedContent = content.length > previewLength
|
||||||
|
? content.substring(0, previewLength) + "..."
|
||||||
|
: content;
|
||||||
|
|
||||||
|
markdownContainer.innerHTML = marked.parse(shortenedContent);
|
||||||
|
this.textContent = content.length > previewLength ? 'Show even more' : 'Show less';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply syntax highlighting
|
||||||
|
previewEl.querySelectorAll('pre code').forEach(block => {
|
||||||
|
hljs.highlightElement(block);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// View toggle functionality
|
// View toggle functionality
|
||||||
const gridViewBtn = document.getElementById('grid-view-btn');
|
const gridViewBtn = document.getElementById('grid-view-btn');
|
||||||
const listViewBtn = document.getElementById('list-view-btn');
|
const listViewBtn = document.getElementById('list-view-btn');
|
||||||
|
@ -355,6 +631,103 @@
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Delete document functionality
|
||||||
|
const deleteModal = document.getElementById('delete-document-modal');
|
||||||
|
const documentNameSpan = document.getElementById('document-delete-name');
|
||||||
|
const cancelDeleteBtn = document.getElementById('cancel-delete-doc');
|
||||||
|
const confirmDeleteBtn = document.getElementById('confirm-delete-doc');
|
||||||
|
let currentDocId = null;
|
||||||
|
|
||||||
|
// Show delete confirmation modal
|
||||||
|
document.querySelectorAll('.delete-document-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
const docId = this.getAttribute('data-doc-id');
|
||||||
|
const docTitle = this.getAttribute('data-doc-title');
|
||||||
|
|
||||||
|
documentNameSpan.textContent = docTitle;
|
||||||
|
currentDocId = docId;
|
||||||
|
deleteModal.classList.remove('hidden');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cancel document deletion
|
||||||
|
cancelDeleteBtn.addEventListener('click', function() {
|
||||||
|
deleteModal.classList.add('hidden');
|
||||||
|
currentDocId = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Confirm document deletion
|
||||||
|
confirmDeleteBtn.addEventListener('click', function() {
|
||||||
|
if (currentDocId) {
|
||||||
|
// Send delete request to server
|
||||||
|
fetch(`/api/document/${currentDocId}`, {
|
||||||
|
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 document');
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
// Remove the document from the DOM
|
||||||
|
document.querySelectorAll(`[data-doc-id="${currentDocId}"]`).forEach(el => {
|
||||||
|
const card = el.closest('.document-card, .document-item');
|
||||||
|
if (card) {
|
||||||
|
card.remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close the modal
|
||||||
|
deleteModal.classList.add('hidden');
|
||||||
|
|
||||||
|
// Show a success message
|
||||||
|
showNotification('Document deleted successfully');
|
||||||
|
|
||||||
|
// Update count in the heading
|
||||||
|
const countEl = document.querySelector('h2');
|
||||||
|
if (countEl) {
|
||||||
|
const count = document.querySelectorAll('.document-card').length;
|
||||||
|
countEl.textContent = `All Documents (${count})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show "no results" if there are no documents left
|
||||||
|
if (document.querySelectorAll('.document-card').length === 0) {
|
||||||
|
noResults.classList.remove('hidden');
|
||||||
|
gridView.classList.add('hidden');
|
||||||
|
listView.classList.add('hidden');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
showNotification('Error deleting document', 'error');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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`;
|
||||||
|
notification.textContent = message;
|
||||||
|
|
||||||
|
document.body.appendChild(notification);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.classList.add('translate-y-16', 'opacity-0');
|
||||||
|
setTimeout(() => notification.remove(), 300);
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
// Highlight current category in sidebar
|
// Highlight current category in sidebar
|
||||||
const sidebarCategories = document.querySelectorAll('.category-item');
|
const sidebarCategories = document.querySelectorAll('.category-item');
|
||||||
sidebarCategories.forEach(item => {
|
sidebarCategories.forEach(item => {
|
||||||
|
|
|
@ -5,9 +5,17 @@
|
||||||
{% block header_title %}{% if category %}Edit Category{% else %}New Category{% endif %}{% endblock %}
|
{% block header_title %}{% if category %}Edit Category{% else %}New Category{% endif %}{% endblock %}
|
||||||
|
|
||||||
{% block header_actions %}
|
{% block header_actions %}
|
||||||
<button id="save-category" class="inline-flex items-center px-4 py-2 bg-primary text-black rounded-md hover:bg-primary-dark transition-colors">
|
<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
|
<i class="mdi mdi-content-save mr-2"></i> Save
|
||||||
</button>
|
</button>
|
||||||
|
<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>
|
||||||
|
{% 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
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
@ -181,12 +189,65 @@
|
||||||
<i class="mdi mdi-check-circle mr-2"></i>
|
<i class="mdi mdi-check-circle mr-2"></i>
|
||||||
<span>Category saved successfully!</span>
|
<span>Category saved successfully!</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Delete Category Modal -->
|
||||||
|
<div id="delete-category-modal" class="fixed inset-0 bg-black/70 z-50 flex items-center justify-center hidden">
|
||||||
|
<div class="bg-gray-800 rounded-lg shadow-lg w-full max-w-md mx-4">
|
||||||
|
<div class="flex items-center justify-between p-4 border-b border-gray-700">
|
||||||
|
<h3 class="text-lg font-medium text-white">Delete Category</h3>
|
||||||
|
<button id="close-delete-modal" class="text-gray-400 hover:text-white">
|
||||||
|
<i class="mdi mdi-close text-lg"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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) %}
|
||||||
|
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) %}
|
||||||
|
<div class="mb-6">
|
||||||
|
<p class="text-white mb-2">What should happen to the contents?</p>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<label class="flex items-start">
|
||||||
|
<input type="radio" name="delete-option" value="move" class="mt-1 mr-2" checked>
|
||||||
|
<div>
|
||||||
|
<span class="text-gray-300">Move contents to parent/root category</span>
|
||||||
|
<p class="text-xs text-gray-500">Documents and subcategories will be preserved and moved to the parent category</p>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
<label class="flex items-start">
|
||||||
|
<input type="radio" name="delete-option" value="delete" class="mt-1 mr-2">
|
||||||
|
<div>
|
||||||
|
<span class="text-gray-300">Delete all contents</span>
|
||||||
|
<p class="text-xs text-gray-500">All documents and subcategories will be permanently deleted</p>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="flex justify-end space-x-3">
|
||||||
|
<button id="cancel-delete-btn" class="px-4 py-2 bg-gray-700 text-white rounded-md hover:bg-gray-600 transition-colors">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button id="confirm-delete-btn" class="px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-500 transition-colors">
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extra_js %}
|
{% block extra_js %}
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
const saveButton = document.getElementById('save-category');
|
const saveButton = document.getElementById('save-button');
|
||||||
const nameInput = document.getElementById('category-name');
|
const nameInput = document.getElementById('category-name');
|
||||||
const iconInput = document.getElementById('category-icon');
|
const iconInput = document.getElementById('category-icon');
|
||||||
const descriptionInput = document.getElementById('category-description');
|
const descriptionInput = document.getElementById('category-description');
|
||||||
|
@ -444,6 +505,93 @@
|
||||||
iconSearchInput.addEventListener('input', function() {
|
iconSearchInput.addEventListener('input', function() {
|
||||||
populateIconsGrid(this.value);
|
populateIconsGrid(this.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Delete Category Functionality
|
||||||
|
const deleteBtn = document.getElementById('delete-category-btn');
|
||||||
|
const deleteModal = document.getElementById('delete-category-modal');
|
||||||
|
const closeDeleteModalBtn = document.getElementById('close-delete-modal');
|
||||||
|
const cancelDeleteBtn = document.getElementById('cancel-delete-btn');
|
||||||
|
const confirmDeleteBtn = document.getElementById('confirm-delete-btn');
|
||||||
|
|
||||||
|
if (deleteBtn) {
|
||||||
|
deleteBtn.addEventListener('click', function() {
|
||||||
|
deleteModal.classList.remove('hidden');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (closeDeleteModalBtn) {
|
||||||
|
closeDeleteModalBtn.addEventListener('click', function() {
|
||||||
|
deleteModal.classList.add('hidden');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cancelDeleteBtn) {
|
||||||
|
cancelDeleteBtn.addEventListener('click', function() {
|
||||||
|
deleteModal.classList.add('hidden');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (confirmDeleteBtn) {
|
||||||
|
confirmDeleteBtn.addEventListener('click', function() {
|
||||||
|
// Get selected option
|
||||||
|
const deleteOption = document.querySelector('input[name="delete-option"]:checked')?.value || 'move';
|
||||||
|
const queryParams = deleteOption === 'delete' ? '' : '?preserve_contents=true';
|
||||||
|
|
||||||
|
// Send delete request
|
||||||
|
fetch(`/api/category/{{ category.id if category else '' }}${queryParams}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRFToken': '{{ csrf_token() }}'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Server responded with an error: ' + response.status);
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
console.log("Category deleted successfully:", data);
|
||||||
|
|
||||||
|
// Show save notification
|
||||||
|
const notification = document.createElement('div');
|
||||||
|
notification.className = 'fixed bottom-4 right-4 bg-primary/90 text-white px-4 py-2 rounded-md shadow-lg transform translate-y-0 opacity-100 transition-all duration-300';
|
||||||
|
notification.innerHTML = '<i class="mdi mdi-check-circle mr-2"></i> Category deleted successfully';
|
||||||
|
document.body.appendChild(notification);
|
||||||
|
|
||||||
|
// Hide notification after 3 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.classList.add('translate-y-16', 'opacity-0');
|
||||||
|
setTimeout(() => notification.remove(), 300);
|
||||||
|
}, 3000);
|
||||||
|
|
||||||
|
// Redirect to home page or parent category
|
||||||
|
setTimeout(() => {
|
||||||
|
{% if category and category.parent_id %}
|
||||||
|
window.location.href = '/category/{{ category.parent_id }}';
|
||||||
|
{% else %}
|
||||||
|
window.location.href = '/';
|
||||||
|
{% endif %}
|
||||||
|
}, 1000);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error deleting category:', error);
|
||||||
|
|
||||||
|
// Show error notification
|
||||||
|
const notification = document.createElement('div');
|
||||||
|
notification.className = 'fixed bottom-4 right-4 bg-red-600/90 text-white px-4 py-2 rounded-md shadow-lg transform translate-y-0 opacity-100 transition-all duration-300';
|
||||||
|
notification.innerHTML = '<i class="mdi mdi-alert-circle mr-2"></i> Error deleting category';
|
||||||
|
document.body.appendChild(notification);
|
||||||
|
|
||||||
|
// Hide notification after 3 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.classList.add('translate-y-16', 'opacity-0');
|
||||||
|
setTimeout(() => notification.remove(), 300);
|
||||||
|
}, 3000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
Loading…
Add table
Add a link
Reference in a new issue