working preview.. for view
This commit is contained in:
parent
9e2d3c9707
commit
17885b005c
4 changed files with 652 additions and 241 deletions
|
@ -62,6 +62,39 @@ def view_category(category_id):
|
||||||
category = Category.query.filter_by(id=category_id, user_id=current_user.id).first_or_404()
|
category = Category.query.filter_by(id=category_id, user_id=current_user.id).first_or_404()
|
||||||
return render_template('category.html', category=category)
|
return render_template('category.html', category=category)
|
||||||
|
|
||||||
|
@main.route('/category/<int:category_id>/all_documents')
|
||||||
|
@login_required
|
||||||
|
def category_all_documents(category_id):
|
||||||
|
"""View all documents in a category, including those in subcategories"""
|
||||||
|
category = Category.query.filter_by(id=category_id, user_id=current_user.id).first_or_404()
|
||||||
|
|
||||||
|
# Get all documents from this category
|
||||||
|
documents = []
|
||||||
|
|
||||||
|
# Add documents from the current category
|
||||||
|
for doc in category.documents:
|
||||||
|
documents.append(doc)
|
||||||
|
|
||||||
|
# Helper function to recursively collect documents from subcategories
|
||||||
|
def collect_documents(cat):
|
||||||
|
docs = []
|
||||||
|
for doc in cat.documents:
|
||||||
|
docs.append(doc)
|
||||||
|
|
||||||
|
for child in cat.children:
|
||||||
|
docs.extend(collect_documents(child))
|
||||||
|
|
||||||
|
return docs
|
||||||
|
|
||||||
|
# Collect documents from all subcategories
|
||||||
|
for subcategory in category.children:
|
||||||
|
documents.extend(collect_documents(subcategory))
|
||||||
|
|
||||||
|
# Sort by updated_date, with most recent first
|
||||||
|
documents.sort(key=lambda x: x.updated_date, reverse=True)
|
||||||
|
|
||||||
|
return render_template('category_documents.html', category=category, documents=documents)
|
||||||
|
|
||||||
@main.route('/document/<int:doc_id>', methods=['GET'])
|
@main.route('/document/<int:doc_id>', methods=['GET'])
|
||||||
@login_required
|
@login_required
|
||||||
def view_document(doc_id):
|
def view_document(doc_id):
|
||||||
|
@ -198,6 +231,13 @@ def delete_document(doc_id):
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return jsonify({'success': True})
|
return jsonify({'success': True})
|
||||||
|
|
||||||
|
@main.route('/api/document/<int:doc_id>', methods=['GET'])
|
||||||
|
@login_required
|
||||||
|
def get_document(doc_id):
|
||||||
|
"""Get a document by ID"""
|
||||||
|
document = Document.query.filter_by(id=doc_id, user_id=current_user.id).first_or_404()
|
||||||
|
return jsonify(document.to_dict())
|
||||||
|
|
||||||
@main.route('/api/document/<int:doc_id>', methods=['PATCH'])
|
@main.route('/api/document/<int:doc_id>', methods=['PATCH'])
|
||||||
@login_required
|
@login_required
|
||||||
def update_document(doc_id):
|
def update_document(doc_id):
|
||||||
|
|
|
@ -13,9 +13,12 @@
|
||||||
<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">
|
<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
|
<i class="mdi mdi-folder-plus-outline mr-2"></i> New Subcategory
|
||||||
</a>
|
</a>
|
||||||
<button id="edit-category-btn" class="inline-flex items-center px-4 py-2 bg-gray-700 text-white rounded-md hover:bg-gray-600 transition-colors ml-2">
|
<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
|
<i class="mdi mdi-pencil mr-2"></i> Edit Category
|
||||||
</button>
|
</a>
|
||||||
|
<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>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
@ -85,14 +88,14 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
<!-- Add subcategory card -->
|
<!-- Add subcategory card -->
|
||||||
<div id="add-subcategory-card" class="bg-gray-800/50 border-2 border-dashed border-gray-700 rounded-lg overflow-hidden hover:border-primary/50 hover:bg-gray-800/80 transition-all cursor-pointer">
|
<div class="bg-gray-800/50 border-2 border-dashed border-gray-700 rounded-lg overflow-hidden hover:border-primary/50 hover:bg-gray-800/80 transition-all cursor-pointer">
|
||||||
<div class="p-5 h-full flex flex-col items-center justify-center text-center">
|
<a href="{{ url_for('main.new_category') }}?parent_id={{ category.id }}" class="block p-5 h-full flex flex-col items-center justify-center text-center">
|
||||||
<div class="w-12 h-12 rounded-full bg-gray-700/50 flex items-center justify-center mb-3">
|
<div class="w-12 h-12 rounded-full bg-gray-700/50 flex items-center justify-center mb-3">
|
||||||
<i class="mdi mdi-folder-plus-outline text-2xl text-gray-500"></i>
|
<i class="mdi mdi-folder-plus-outline text-2xl text-gray-500"></i>
|
||||||
</div>
|
</div>
|
||||||
<h3 class="text-gray-400 font-medium mb-1">New Subcategory</h3>
|
<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.name }}</p>
|
||||||
</div>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -102,12 +105,25 @@
|
||||||
<div>
|
<div>
|
||||||
<div class="flex items-center justify-between mb-4">
|
<div class="flex items-center justify-between mb-4">
|
||||||
<h2 class="text-xl font-semibold text-white">Documents</h2>
|
<h2 class="text-xl font-semibold text-white">Documents</h2>
|
||||||
<a href="{{ url_for('main.new_document') }}?category={{ category.id }}" class="icon-button" title="New Document">
|
|
||||||
<i class="mdi mdi-plus"></i>
|
<!-- View toggle buttons -->
|
||||||
</a>
|
<div class="flex items-center">
|
||||||
|
<div class="flex bg-gray-700 rounded-md p-1 mr-2">
|
||||||
|
<button id="grid-view-btn" class="px-3 py-1 rounded text-sm flex items-center active-view">
|
||||||
|
<i class="mdi mdi-view-grid mr-1"></i> Grid
|
||||||
|
</button>
|
||||||
|
<button id="list-view-btn" class="px-3 py-1 rounded text-sm flex items-center">
|
||||||
|
<i class="mdi mdi-view-list mr-1"></i> List
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<a href="{{ url_for('main.new_document') }}?category={{ category.id }}" class="icon-button" title="New Document">
|
||||||
|
<i class="mdi mdi-plus"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
<!-- Grid view (default) -->
|
||||||
|
<div id="grid-view" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
{% if category.documents.count() > 0 %}
|
{% if category.documents.count() > 0 %}
|
||||||
{% for doc in category.documents %}
|
{% for doc in category.documents %}
|
||||||
<div class="bg-gray-800 rounded-lg overflow-hidden shadow hover:shadow-lg transition-shadow">
|
<div class="bg-gray-800 rounded-lg overflow-hidden shadow hover:shadow-lg transition-shadow">
|
||||||
|
@ -164,80 +180,59 @@
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Category Modal -->
|
|
||||||
<div id="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 id="modal-title" class="text-lg font-medium text-white">Add Subcategory</h3>
|
|
||||||
<button class="close-modal text-gray-400 hover:text-white">
|
|
||||||
<i class="mdi mdi-close text-lg"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="p-6">
|
<!-- List view (initially hidden) -->
|
||||||
<form id="category-form" class="space-y-4">
|
<div id="list-view" class="hidden">
|
||||||
<input type="hidden" id="category-id" value="">
|
<div class="bg-gray-800 rounded-lg shadow">
|
||||||
|
{% if category.documents.count() > 0 %}
|
||||||
<div>
|
<div class="divide-y divide-gray-700">
|
||||||
<label for="category-name" class="block text-sm font-medium text-gray-400 mb-1">Name</label>
|
{% for doc in category.documents %}
|
||||||
<input type="text" id="category-name" 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">
|
<div class="document-list-item group">
|
||||||
</div>
|
<div class="flex items-center justify-between py-3 px-4 hover:bg-gray-700/50 transition-colors">
|
||||||
|
<div class="flex items-center flex-1 min-w-0">
|
||||||
<div>
|
<i class="mdi mdi-file-document-outline text-gray-400 mr-3"></i>
|
||||||
<label for="category-icon" class="block text-sm font-medium text-gray-400 mb-1">Icon</label>
|
<div class="flex-1 min-w-0">
|
||||||
<div class="flex items-center">
|
<a href="{{ url_for('main.view_document', doc_id=doc.id) }}" class="text-gray-300 hover:text-primary transition-colors block font-medium truncate">{{ doc.title }}</a>
|
||||||
<input type="text" id="category-icon" value="mdi-folder-outline" class="flex-1 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">
|
<div class="mt-1 flex items-center text-xs text-gray-500">
|
||||||
<div class="ml-3 w-10 h-10 flex items-center justify-center text-primary bg-primary/10 rounded-md">
|
<i class="mdi mdi-calendar-outline mr-1"></i>
|
||||||
<i id="icon-preview" class="mdi mdi-folder-outline text-xl"></i>
|
<span>{{ doc.updated_date.strftime('%b %d, %Y') }}</span>
|
||||||
|
{% if doc.tags %}
|
||||||
|
<span class="mx-2">•</span>
|
||||||
|
<div class="flex gap-1">
|
||||||
|
{% for tag in doc.tags %}
|
||||||
|
<span class="text-xs px-1.5 py-0.5 bg-primary/20 text-primary rounded-full">{{ tag.name }}</span>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2 hidden group-hover:flex">
|
||||||
|
<a href="{{ url_for('main.view_document', doc_id=doc.id) }}" class="p-1 text-gray-400 hover:text-primary rounded transition-all" title="View Document">
|
||||||
|
<i class="mdi mdi-eye-outline"></i>
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('main.edit_document', doc_id=doc.id) }}" class="p-1 text-gray-400 hover:text-primary rounded transition-all" title="Edit Document">
|
||||||
|
<i class="mdi mdi-pencil-outline"></i>
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('main.export_document', doc_id=doc.id) }}" class="p-1 text-gray-400 hover:text-primary rounded transition-all" title="Export Document">
|
||||||
|
<i class="mdi mdi-download-outline"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-2 flex flex-wrap gap-2">
|
{% else %}
|
||||||
<button type="button" data-icon="mdi-folder-outline" class="icon-select w-8 h-8 flex items-center justify-center bg-gray-700 rounded-md text-primary hover:bg-gray-600">
|
<div class="p-8 text-center">
|
||||||
<i class="mdi mdi-folder-outline"></i>
|
<i class="mdi mdi-file-document-outline text-6xl text-gray-700 mb-3"></i>
|
||||||
</button>
|
<h3 class="text-lg text-gray-400 mb-3">No documents in this category</h3>
|
||||||
<button type="button" data-icon="mdi-folder-text-outline" class="icon-select w-8 h-8 flex items-center justify-center bg-gray-700 rounded-md text-primary hover:bg-gray-600">
|
<p class="text-gray-500 mb-4">Create your first document in this category</p>
|
||||||
<i class="mdi mdi-folder-text-outline"></i>
|
<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">
|
||||||
</button>
|
<i class="mdi mdi-plus mr-2"></i> Create Document
|
||||||
<button type="button" data-icon="mdi-code-braces" class="icon-select w-8 h-8 flex items-center justify-center bg-gray-700 rounded-md text-primary hover:bg-gray-600">
|
</a>
|
||||||
<i class="mdi mdi-code-braces"></i>
|
|
||||||
</button>
|
|
||||||
<button type="button" data-icon="mdi-database" class="icon-select w-8 h-8 flex items-center justify-center bg-gray-700 rounded-md text-primary hover:bg-gray-600">
|
|
||||||
<i class="mdi mdi-database"></i>
|
|
||||||
</button>
|
|
||||||
<button type="button" data-icon="mdi-web" class="icon-select w-8 h-8 flex items-center justify-center bg-gray-700 rounded-md text-primary hover:bg-gray-600">
|
|
||||||
<i class="mdi mdi-web"></i>
|
|
||||||
</button>
|
|
||||||
<button type="button" data-icon="mdi-book-outline" class="icon-select w-8 h-8 flex items-center justify-center bg-gray-700 rounded-md text-primary hover:bg-gray-600">
|
|
||||||
<i class="mdi mdi-book-outline"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{% endif %}
|
||||||
|
</div>
|
||||||
<div>
|
|
||||||
<label for="category-description" class="block text-sm font-medium text-gray-400 mb-1">Description (Optional)</label>
|
|
||||||
<input type="text" id="category-description" 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">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<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</option>
|
|
||||||
<option value="{{ category.id }}" selected>{{ category.name }}</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="pt-4 flex justify-end space-x-3">
|
|
||||||
<button type="button" id="cancel-btn" class="px-4 py-2 bg-gray-700 text-white rounded-md hover:bg-gray-600 transition-colors">
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button type="submit" class="px-4 py-2 bg-primary text-black rounded-md hover:bg-primary-dark transition-colors">
|
|
||||||
Save
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -246,102 +241,6 @@
|
||||||
{% block extra_js %}
|
{% block extra_js %}
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
const modal = document.getElementById('category-modal');
|
|
||||||
const addBtn = document.getElementById('add-subcategory-btn');
|
|
||||||
const addCard = document.getElementById('add-subcategory-card');
|
|
||||||
const editBtn = document.getElementById('edit-category-btn');
|
|
||||||
const closeBtn = document.querySelector('.close-modal');
|
|
||||||
const cancelBtn = document.getElementById('cancel-btn');
|
|
||||||
const form = document.getElementById('category-form');
|
|
||||||
const modalTitle = document.getElementById('modal-title');
|
|
||||||
const iconInput = document.getElementById('category-icon');
|
|
||||||
const iconPreview = document.getElementById('icon-preview');
|
|
||||||
const iconSelectBtns = document.querySelectorAll('.icon-select');
|
|
||||||
|
|
||||||
// Open modal
|
|
||||||
function openModal(isEdit = false) {
|
|
||||||
if (isEdit) {
|
|
||||||
// Edit the current category
|
|
||||||
modalTitle.textContent = 'Edit Category';
|
|
||||||
document.getElementById('category-id').value = '{{ category.id }}';
|
|
||||||
document.getElementById('category-name').value = '{{ category.name }}';
|
|
||||||
document.getElementById('category-icon').value = '{{ category.icon }}';
|
|
||||||
document.getElementById('category-parent').value = '{{ category.parent_id }}' || '';
|
|
||||||
iconPreview.className = 'mdi {{ category.icon }} text-xl';
|
|
||||||
} else {
|
|
||||||
// Add new subcategory
|
|
||||||
modalTitle.textContent = 'Add Subcategory';
|
|
||||||
document.getElementById('category-id').value = '';
|
|
||||||
document.getElementById('category-name').value = '';
|
|
||||||
document.getElementById('category-icon').value = 'mdi-folder-outline';
|
|
||||||
document.getElementById('category-parent').value = '{{ category.id }}';
|
|
||||||
iconPreview.className = 'mdi mdi-folder-outline text-xl';
|
|
||||||
}
|
|
||||||
|
|
||||||
modal.classList.remove('hidden');
|
|
||||||
document.getElementById('category-name').focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close modal
|
|
||||||
function closeModal() {
|
|
||||||
modal.classList.add('hidden');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update icon preview
|
|
||||||
if (iconInput) {
|
|
||||||
iconInput.addEventListener('input', function() {
|
|
||||||
iconPreview.className = 'mdi ' + this.value + ' text-xl';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Icon selection
|
|
||||||
iconSelectBtns.forEach(btn => {
|
|
||||||
btn.addEventListener('click', function() {
|
|
||||||
const icon = this.getAttribute('data-icon');
|
|
||||||
iconInput.value = icon;
|
|
||||||
iconPreview.className = 'mdi ' + icon + ' text-xl';
|
|
||||||
|
|
||||||
// Highlight selected icon
|
|
||||||
iconSelectBtns.forEach(b => b.classList.remove('ring-2', 'ring-primary'));
|
|
||||||
this.classList.add('ring-2', 'ring-primary');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Event listeners
|
|
||||||
if (addBtn) addBtn.addEventListener('click', () => openModal(false));
|
|
||||||
if (addCard) addCard.addEventListener('click', () => openModal(false));
|
|
||||||
if (editBtn) editBtn.addEventListener('click', () => openModal(true));
|
|
||||||
if (closeBtn) closeBtn.addEventListener('click', closeModal);
|
|
||||||
if (cancelBtn) cancelBtn.addEventListener('click', closeModal);
|
|
||||||
|
|
||||||
// Form submission
|
|
||||||
if (form) {
|
|
||||||
form.addEventListener('submit', function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
const formData = {
|
|
||||||
id: document.getElementById('category-id').value || null,
|
|
||||||
name: document.getElementById('category-name').value,
|
|
||||||
icon: document.getElementById('category-icon').value,
|
|
||||||
description: document.getElementById('category-description').value,
|
|
||||||
parent_id: document.getElementById('category-parent').value || null
|
|
||||||
};
|
|
||||||
|
|
||||||
fetch('/api/category', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify(formData)
|
|
||||||
})
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
window.location.reload();
|
|
||||||
})
|
|
||||||
.catch(error => console.error('Error:', error));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dropdown functionality for document cards
|
// Dropdown functionality for document cards
|
||||||
document.querySelectorAll('.dropdown button').forEach(btn => {
|
document.querySelectorAll('.dropdown button').forEach(btn => {
|
||||||
btn.addEventListener('click', function(e) {
|
btn.addEventListener('click', function(e) {
|
||||||
|
@ -362,6 +261,42 @@
|
||||||
menu.classList.add('hidden');
|
menu.classList.add('hidden');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// View toggle functionality
|
||||||
|
const gridViewBtn = document.getElementById('grid-view-btn');
|
||||||
|
const listViewBtn = document.getElementById('list-view-btn');
|
||||||
|
const gridView = document.getElementById('grid-view');
|
||||||
|
const listView = document.getElementById('list-view');
|
||||||
|
|
||||||
|
if (gridViewBtn && listViewBtn && gridView && listView) {
|
||||||
|
// Load view preference from localStorage
|
||||||
|
const viewPreference = localStorage.getItem('categoryViewPreference') || 'grid';
|
||||||
|
|
||||||
|
// Set initial view based on preference
|
||||||
|
if (viewPreference === 'list') {
|
||||||
|
gridView.classList.add('hidden');
|
||||||
|
listView.classList.remove('hidden');
|
||||||
|
gridViewBtn.classList.remove('active-view');
|
||||||
|
listViewBtn.classList.add('active-view');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle views
|
||||||
|
gridViewBtn.addEventListener('click', function() {
|
||||||
|
gridView.classList.remove('hidden');
|
||||||
|
listView.classList.add('hidden');
|
||||||
|
gridViewBtn.classList.add('active-view');
|
||||||
|
listViewBtn.classList.remove('active-view');
|
||||||
|
localStorage.setItem('categoryViewPreference', 'grid');
|
||||||
|
});
|
||||||
|
|
||||||
|
listViewBtn.addEventListener('click', function() {
|
||||||
|
listView.classList.remove('hidden');
|
||||||
|
gridView.classList.add('hidden');
|
||||||
|
listViewBtn.classList.add('active-view');
|
||||||
|
gridViewBtn.classList.remove('active-view');
|
||||||
|
localStorage.setItem('categoryViewPreference', 'list');
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
379
app/templates/category_documents.html
Normal file
379
app/templates/category_documents.html
Normal file
|
@ -0,0 +1,379 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}All Documents - {{ category.name }} - Vim Docs{% endblock %}
|
||||||
|
|
||||||
|
{% block header_title %}
|
||||||
|
<a href="{{ url_for('main.view_category', category_id=category.id) }}" class="hover:underline flex items-center">
|
||||||
|
<i class="mdi {{ category.icon }} mr-2"></i> {{ category.name }} <span class="mx-2">›</span> All Documents
|
||||||
|
</a>
|
||||||
|
{% 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.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-folder-outline mr-2"></i> Back to Category
|
||||||
|
</a>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_css %}
|
||||||
|
<style>
|
||||||
|
.documents-container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.document-card {
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.document-card:hover {
|
||||||
|
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.document-category {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #8b949e;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.document-category i {
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.document-preview {
|
||||||
|
max-height: 4.5rem;
|
||||||
|
overflow: hidden;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 3;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.document-tag {
|
||||||
|
background-color: rgba(80, 250, 123, 0.1);
|
||||||
|
color: #50fa7b;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.document-tag:hover {
|
||||||
|
background-color: rgba(80, 250, 123, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-toggle {
|
||||||
|
display: flex;
|
||||||
|
background-color: #2d333b;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
padding: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-toggle-btn {
|
||||||
|
padding: 0.375rem 0.75rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-toggle-btn i {
|
||||||
|
margin-right: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-toggle-btn.active {
|
||||||
|
background-color: rgba(80, 250, 123, 0.2);
|
||||||
|
color: #50fa7b;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="documents-container">
|
||||||
|
<div class="flex items-center justify-between mb-6">
|
||||||
|
<h2 class="text-xl font-semibold text-white">All Documents ({{ documents|length }})</h2>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<!-- Search input -->
|
||||||
|
<div class="relative">
|
||||||
|
<input type="text" id="document-search" placeholder="Search in documents..." class="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 pr-10">
|
||||||
|
<i class="mdi mdi-magnify absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400"></i>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- View toggle -->
|
||||||
|
<div class="view-toggle">
|
||||||
|
<button id="grid-view-btn" class="view-toggle-btn active">
|
||||||
|
<i class="mdi mdi-view-grid"></i> Grid
|
||||||
|
</button>
|
||||||
|
<button id="list-view-btn" class="view-toggle-btn">
|
||||||
|
<i class="mdi mdi-view-list"></i> List
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if documents %}
|
||||||
|
<!-- Grid view (default) -->
|
||||||
|
<div id="grid-view" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
{% for doc in documents %}
|
||||||
|
<div class="document-card bg-gray-800 rounded-lg overflow-hidden shadow" data-title="{{ doc.title|lower }}" data-content="{{ doc.content|lower }}">
|
||||||
|
<div class="p-5">
|
||||||
|
<div class="flex items-start justify-between mb-2">
|
||||||
|
<h3 class="text-white font-medium truncate w-5/6">
|
||||||
|
<a href="{{ url_for('main.view_document', doc_id=doc.id) }}" class="hover:text-primary transition-colors">
|
||||||
|
{{ doc.title }}
|
||||||
|
</a>
|
||||||
|
</h3>
|
||||||
|
<div class="dropdown relative">
|
||||||
|
<button class="p-1 text-gray-400 hover:text-white rounded">
|
||||||
|
<i class="mdi mdi-dots-vertical"></i>
|
||||||
|
</button>
|
||||||
|
<div class="dropdown-menu hidden absolute right-0 mt-2 w-48 bg-gray-700 rounded shadow-lg z-10">
|
||||||
|
<a href="{{ url_for('main.view_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-eye-outline mr-2"></i> View
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('main.edit_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-pencil-outline mr-2"></i> Edit
|
||||||
|
</a>
|
||||||
|
<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
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if doc.category %}
|
||||||
|
<div class="document-category mb-2">
|
||||||
|
<i class="mdi {{ doc.category.icon }}"></i>
|
||||||
|
<a href="{{ url_for('main.view_category', category_id=doc.category.id) }}" class="hover:text-primary">
|
||||||
|
{{ doc.category.name }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="document-preview text-gray-400 text-sm mb-3">
|
||||||
|
{{ doc.content[:200] }}{% if doc.content|length > 200 %}...{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between mt-3 text-xs text-gray-500">
|
||||||
|
<div>
|
||||||
|
<i class="mdi mdi-clock-outline mr-1"></i>
|
||||||
|
{{ doc.updated_date.strftime('%b %d, %Y') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if doc.tags %}
|
||||||
|
<div class="flex flex-wrap gap-1 mt-3">
|
||||||
|
{% for tag in doc.tags %}
|
||||||
|
<span class="document-tag">{{ tag.name }}</span>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- List view (initially hidden) -->
|
||||||
|
<div id="list-view" class="hidden bg-gray-800 rounded-lg shadow overflow-hidden">
|
||||||
|
<div class="divide-y divide-gray-700">
|
||||||
|
{% for doc in documents %}
|
||||||
|
<div class="document-item p-4 hover:bg-gray-700 transition-colors" data-title="{{ doc.title|lower }}" data-content="{{ doc.content|lower }}">
|
||||||
|
<div class="flex flex-col sm:flex-row items-start sm:items-center justify-between">
|
||||||
|
<div class="flex-1 min-w-0 mr-4">
|
||||||
|
<div class="flex items-center mb-1">
|
||||||
|
<h3 class="text-white font-medium truncate">
|
||||||
|
<a href="{{ url_for('main.view_document', doc_id=doc.id) }}" class="hover:text-primary transition-colors">
|
||||||
|
{{ doc.title }}
|
||||||
|
</a>
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center text-xs text-gray-500 mb-2">
|
||||||
|
{% if doc.category %}
|
||||||
|
<span class="document-category mr-3">
|
||||||
|
<i class="mdi {{ doc.category.icon }}"></i>
|
||||||
|
<a href="{{ url_for('main.view_category', category_id=doc.category.id) }}" class="hover:text-primary">
|
||||||
|
{{ doc.category.name }}
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<span class="mr-3">
|
||||||
|
<i class="mdi mdi-clock-outline mr-1"></i>
|
||||||
|
{{ doc.updated_date.strftime('%b %d, %Y') }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{% if doc.tags %}
|
||||||
|
<div class="flex flex-wrap gap-1">
|
||||||
|
{% for tag in doc.tags %}
|
||||||
|
<span class="document-tag">{{ tag.name }}</span>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="document-preview text-gray-400 text-sm">
|
||||||
|
{{ doc.content[:200] }}{% if doc.content|length > 200 %}...{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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">
|
||||||
|
<i class="mdi mdi-eye-outline"></i>
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('main.edit_document', doc_id=doc.id) }}" class="p-2 text-gray-400 hover:text-primary rounded" title="Edit">
|
||||||
|
<i class="mdi mdi-pencil-outline"></i>
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('main.export_document', doc_id=doc.id) }}" class="p-2 text-gray-400 hover:text-primary rounded" title="Export">
|
||||||
|
<i class="mdi mdi-download-outline"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% else %}
|
||||||
|
<div class="bg-gray-800/50 rounded-lg p-8 text-center">
|
||||||
|
<i class="mdi mdi-file-document-outline text-6xl text-gray-700 mb-3"></i>
|
||||||
|
<h3 class="text-lg text-gray-400 mb-3">No documents found</h3>
|
||||||
|
<p class="text-gray-500 mb-4">This category and its subcategories don't have any documents yet</p>
|
||||||
|
<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> Create Document
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- No results message (hidden by default) -->
|
||||||
|
<div id="no-results" class="hidden bg-gray-800/50 rounded-lg p-8 text-center mt-4">
|
||||||
|
<i class="mdi mdi-file-search-outline text-6xl text-gray-700 mb-3"></i>
|
||||||
|
<h3 class="text-lg text-gray-400 mb-3">No matching documents</h3>
|
||||||
|
<p class="text-gray-500">Try a different search term</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_js %}
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// View toggle functionality
|
||||||
|
const gridViewBtn = document.getElementById('grid-view-btn');
|
||||||
|
const listViewBtn = document.getElementById('list-view-btn');
|
||||||
|
const gridView = document.getElementById('grid-view');
|
||||||
|
const listView = document.getElementById('list-view');
|
||||||
|
|
||||||
|
// Load view preference from localStorage
|
||||||
|
const viewPreference = localStorage.getItem('categoryDocumentsView') || 'grid';
|
||||||
|
|
||||||
|
// Set initial view based on preference
|
||||||
|
if (viewPreference === 'list') {
|
||||||
|
gridView.classList.add('hidden');
|
||||||
|
listView.classList.remove('hidden');
|
||||||
|
gridViewBtn.classList.remove('active');
|
||||||
|
listViewBtn.classList.add('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle views
|
||||||
|
gridViewBtn.addEventListener('click', function() {
|
||||||
|
gridView.classList.remove('hidden');
|
||||||
|
listView.classList.add('hidden');
|
||||||
|
gridViewBtn.classList.add('active');
|
||||||
|
listViewBtn.classList.remove('active');
|
||||||
|
localStorage.setItem('categoryDocumentsView', 'grid');
|
||||||
|
});
|
||||||
|
|
||||||
|
listViewBtn.addEventListener('click', function() {
|
||||||
|
listView.classList.remove('hidden');
|
||||||
|
gridView.classList.add('hidden');
|
||||||
|
listViewBtn.classList.add('active');
|
||||||
|
gridViewBtn.classList.remove('active');
|
||||||
|
localStorage.setItem('categoryDocumentsView', 'list');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Search functionality
|
||||||
|
const searchInput = document.getElementById('document-search');
|
||||||
|
const documentItems = document.querySelectorAll('.document-card, .document-item');
|
||||||
|
const noResults = document.getElementById('no-results');
|
||||||
|
|
||||||
|
searchInput.addEventListener('input', function() {
|
||||||
|
const query = this.value.toLowerCase().trim();
|
||||||
|
let hasResults = false;
|
||||||
|
|
||||||
|
documentItems.forEach(item => {
|
||||||
|
const title = item.getAttribute('data-title');
|
||||||
|
const content = item.getAttribute('data-content');
|
||||||
|
|
||||||
|
if (title.includes(query) || content.includes(query)) {
|
||||||
|
item.style.display = '';
|
||||||
|
hasResults = true;
|
||||||
|
} else {
|
||||||
|
item.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show/hide no results message
|
||||||
|
if (hasResults) {
|
||||||
|
noResults.classList.add('hidden');
|
||||||
|
if (viewPreference === 'grid') {
|
||||||
|
gridView.classList.remove('hidden');
|
||||||
|
} else {
|
||||||
|
listView.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
noResults.classList.remove('hidden');
|
||||||
|
gridView.classList.add('hidden');
|
||||||
|
listView.classList.add('hidden');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Dropdown functionality
|
||||||
|
document.querySelectorAll('.dropdown button').forEach(btn => {
|
||||||
|
btn.addEventListener('click', function(e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
const menu = this.nextElementSibling;
|
||||||
|
menu.classList.toggle('hidden');
|
||||||
|
|
||||||
|
// Close other open dropdowns
|
||||||
|
document.querySelectorAll('.dropdown-menu:not(.hidden)').forEach(m => {
|
||||||
|
if (m !== menu) m.classList.add('hidden');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close dropdowns when clicking outside
|
||||||
|
document.addEventListener('click', function() {
|
||||||
|
document.querySelectorAll('.dropdown-menu:not(.hidden)').forEach(menu => {
|
||||||
|
menu.classList.add('hidden');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Highlight current category in sidebar
|
||||||
|
const sidebarCategories = document.querySelectorAll('.category-item');
|
||||||
|
sidebarCategories.forEach(item => {
|
||||||
|
if (item.dataset.categoryId === "{{ category.id }}") {
|
||||||
|
const categoryLink = item.querySelector('a');
|
||||||
|
if (categoryLink) {
|
||||||
|
categoryLink.classList.add('text-primary');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expand parent category if necessary
|
||||||
|
const parentContainer = item.closest('.category-children');
|
||||||
|
if (parentContainer && parentContainer.style.display === 'none') {
|
||||||
|
const toggleBtn = parentContainer.parentElement.querySelector('.toggle-btn');
|
||||||
|
if (toggleBtn) {
|
||||||
|
toggleBtn.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
|
@ -5,42 +5,55 @@
|
||||||
{% block header_title %}{{ document.title }}{% endblock %}
|
{% block header_title %}{{ document.title }}{% endblock %}
|
||||||
|
|
||||||
{% block header_actions %}
|
{% block header_actions %}
|
||||||
<a href="{{ url_for('main.edit_document', doc_id=document.id) }}" class="button primary">
|
<a href="{{ url_for('main.edit_document', doc_id=document.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-pencil"></i> Edit
|
<i class="mdi mdi-pencil mr-2"></i> Edit
|
||||||
</a>
|
</a>
|
||||||
<a href="{{ url_for('main.export_document', doc_id=document.id) }}" class="button">
|
<a href="{{ url_for('main.export_document', doc_id=document.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-download"></i> Export
|
<i class="mdi mdi-download mr-2"></i> Export
|
||||||
</a>
|
</a>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extra_css %}
|
{% block extra_css %}
|
||||||
<!-- GitHub Markdown CSS -->
|
<!-- GitHub Markdown CSS -->
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.2.0/github-markdown-dark.min.css">
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.2.0/github-markdown-dark.min.css">
|
||||||
|
<!-- Highlight.js for syntax highlighting -->
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/github-dark.min.css">
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.document-view {
|
.document-container {
|
||||||
padding: 20px;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: calc(100vh - 64px);
|
||||||
background-color: #0d1117;
|
background-color: #0d1117;
|
||||||
}
|
}
|
||||||
|
|
||||||
.document-metadata {
|
.document-header {
|
||||||
margin-bottom: 20px;
|
padding: 20px 30px;
|
||||||
padding-bottom: 15px;
|
border-bottom: 1px solid #30363d;
|
||||||
border-bottom: 1px solid var(--border-color);
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.document-metadata {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.metadata-item {
|
.metadata-item {
|
||||||
margin-right: 15px;
|
|
||||||
color: var(--text-muted);
|
|
||||||
font-size: 0.9em;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #8b949e;
|
||||||
}
|
}
|
||||||
|
|
||||||
.metadata-item i {
|
.metadata-item i {
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
|
color: #6e7681;
|
||||||
}
|
}
|
||||||
|
|
||||||
.document-tags {
|
.document-tags {
|
||||||
|
@ -50,16 +63,23 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.document-tag {
|
.document-tag {
|
||||||
background: var(--accent-color);
|
background-color: rgba(80, 250, 123, 0.1);
|
||||||
color: var(--bg-color);
|
color: #50fa7b;
|
||||||
padding: 2px 8px;
|
padding: 2px 8px;
|
||||||
border-radius: 4px;
|
border-radius: 12px;
|
||||||
font-size: 12px;
|
font-size: 0.75rem;
|
||||||
transition: transform 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.document-tag:hover {
|
.document-tag:hover {
|
||||||
transform: translateY(-2px);
|
background-color: rgba(80, 250, 123, 0.2);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.document-body {
|
||||||
|
flex: 1;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body {
|
.markdown-body {
|
||||||
|
@ -68,7 +88,6 @@
|
||||||
max-width: 980px;
|
max-width: 980px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 45px;
|
padding: 45px;
|
||||||
color: #c9d1d9;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* GitHub-style admonitions/alerts */
|
/* GitHub-style admonitions/alerts */
|
||||||
|
@ -130,45 +149,59 @@
|
||||||
color: #f85149;
|
color: #f85149;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Regular blockquotes */
|
.markdown-body .admonition-danger {
|
||||||
.markdown-body blockquote {
|
border-color: #cf222e;
|
||||||
padding: 0.5rem 1rem;
|
background-color: rgba(207, 34, 46, 0.1);
|
||||||
color: #8b949e;
|
|
||||||
border-left: 0.25em solid #30363d;
|
|
||||||
margin: 1em 0;
|
|
||||||
background-color: rgba(55, 65, 81, 0.1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body blockquote > :first-child {
|
.markdown-body .admonition-danger .admonition-title {
|
||||||
margin-top: 0;
|
color: #cf222e;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body blockquote > :last-child {
|
/* Animation for fade-in */
|
||||||
margin-bottom: 0;
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; }
|
||||||
|
to { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-in {
|
||||||
|
animation: fadeIn 0.5s ease-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile adjustments */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.document-header {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body {
|
||||||
|
padding: 25px 15px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="document-view">
|
<div class="document-container">
|
||||||
<div class="document-metadata">
|
<div class="document-header">
|
||||||
<div class="metadata-item">
|
<div class="document-metadata">
|
||||||
<i class="mdi mdi-calendar"></i>
|
<div class="metadata-item">
|
||||||
Created: {{ document.created_date.strftime('%b %d, %Y') }}
|
<i class="mdi mdi-clock-outline"></i>
|
||||||
|
Updated {{ document.updated_date.strftime('%b %d, %Y') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if document.category %}
|
||||||
|
<div class="metadata-item">
|
||||||
|
<i class="mdi {{ document.category.icon }}"></i>
|
||||||
|
<a href="{{ url_for('main.view_category', category_id=document.category.id) }}" class="hover:text-primary transition-colors">
|
||||||
|
{{ document.category.name }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="metadata-item">
|
|
||||||
<i class="mdi mdi-update"></i>
|
|
||||||
Updated: {{ document.updated_date.strftime('%b %d, %Y') }}
|
|
||||||
</div>
|
|
||||||
{% if document.category %}
|
|
||||||
<div class="metadata-item">
|
|
||||||
<i class="mdi {{ document.category.icon }}"></i>
|
|
||||||
{{ document.category.name }}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% if document.tags %}
|
{% if document.tags %}
|
||||||
<div class="metadata-item document-tags">
|
<div class="document-tags">
|
||||||
<i class="mdi mdi-tag-multiple"></i>
|
|
||||||
{% for tag in document.tags %}
|
{% for tag in document.tags %}
|
||||||
<span class="document-tag">{{ tag.name }}</span>
|
<span class="document-tag">{{ tag.name }}</span>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@ -176,8 +209,10 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="markdown-body" id="document-content" data-content="{{ document.content|tojson|safe }}">
|
<div class="document-body">
|
||||||
<!-- Content will be rendered here -->
|
<div id="document-content" class="markdown-body fade-in">
|
||||||
|
<!-- Content will be rendered here -->
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -185,6 +220,8 @@
|
||||||
{% block extra_js %}
|
{% block extra_js %}
|
||||||
<!-- Marked.js for Markdown parsing -->
|
<!-- Marked.js for Markdown parsing -->
|
||||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||||
|
<!-- Highlight.js for syntax highlighting -->
|
||||||
|
<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() {
|
||||||
|
@ -197,7 +234,13 @@
|
||||||
pedantic: false,
|
pedantic: false,
|
||||||
sanitize: false,
|
sanitize: false,
|
||||||
smartLists: true,
|
smartLists: true,
|
||||||
smartypants: false
|
smartypants: 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 GitHub-style alert blocks
|
// Custom renderer for GitHub-style alert blocks
|
||||||
|
@ -239,21 +282,10 @@
|
||||||
|
|
||||||
// Render markdown content
|
// Render markdown content
|
||||||
const documentContent = document.getElementById('document-content');
|
const documentContent = document.getElementById('document-content');
|
||||||
const markdownContent = documentContent.getAttribute('data-content');
|
const markdownContent = `{{ document.content|replace("\n", "\\n")|replace("'", "\\'")|safe }}`;
|
||||||
|
|
||||||
console.log("Raw content attribute:", markdownContent);
|
// Render directly without JSON parsing
|
||||||
|
documentContent.innerHTML = marked.parse(markdownContent);
|
||||||
try {
|
|
||||||
// Parse the JSON-encoded content (Flask's tojson filter wraps content in quotes)
|
|
||||||
const decodedContent = JSON.parse(markdownContent);
|
|
||||||
console.log("Content length:", decodedContent.length);
|
|
||||||
|
|
||||||
// Render the markdown directly without additional checks
|
|
||||||
documentContent.innerHTML = marked.parse(decodedContent);
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Error rendering document:", e);
|
|
||||||
documentContent.innerHTML = '<div class="text-center p-8 text-red-500">Error displaying document content.</div>';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add linkification for dynamic links
|
// Add linkification for dynamic links
|
||||||
document.querySelectorAll('#document-content a[href^="#doc:"]').forEach(link => {
|
document.querySelectorAll('#document-content a[href^="#doc:"]').forEach(link => {
|
||||||
|
@ -264,15 +296,40 @@
|
||||||
link.removeAttribute('rel');
|
link.removeAttribute('rel');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Highlight active document in sidebar
|
||||||
|
highlightActiveDocument();
|
||||||
|
|
||||||
// Add keyboard shortcuts
|
// Add keyboard shortcuts
|
||||||
document.addEventListener('keydown', function(e) {
|
document.addEventListener('keydown', function(e) {
|
||||||
// 'e' to edit the current document
|
// 'e' to edit the current document
|
||||||
if (e.key === 'e' && !e.ctrlKey && !e.metaKey && !e.altKey &&
|
if (e.key === 'e' && !e.ctrlKey && !e.metaKey && !e.altKey &&
|
||||||
!(e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA')) {
|
!(e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA')) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
window.location.href = "{{ url_for('main.edit_document', doc_id=document.id) }}";
|
window.location.href = "{{ url_for('main.edit_document', doc_id=document.id) }}";
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function highlightActiveDocument() {
|
||||||
|
// Find the current document in the sidebar and highlight it
|
||||||
|
const currentDocId = "{{ document.id }}";
|
||||||
|
const sidebarItems = document.querySelectorAll('.document-item a');
|
||||||
|
|
||||||
|
sidebarItems.forEach(item => {
|
||||||
|
const href = item.getAttribute('href');
|
||||||
|
if (href && href.includes(`/document/${currentDocId}`)) {
|
||||||
|
item.classList.add('text-primary');
|
||||||
|
|
||||||
|
// Expand parent category if collapsed
|
||||||
|
const parentContainer = item.closest('.category-children');
|
||||||
|
if (parentContainer && parentContainer.style.display === 'none') {
|
||||||
|
const toggleBtn = parentContainer.parentElement.querySelector('.toggle-btn');
|
||||||
|
if (toggleBtn) {
|
||||||
|
toggleBtn.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
Loading…
Add table
Add a link
Reference in a new issue