flask-vim-docs/app/templates/category_documents.html
2025-04-17 12:05:22 +02:00

752 lines
No EOL
31 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% 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;
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 {
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;
}
.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>
{% 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-view">
<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>
<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>
{% 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">
<div class="markdown-content" data-content="{{ doc.content }}">
<!-- Markdown will be rendered here -->
</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>
<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">
<div class="markdown-content" data-content="{{ doc.content }}">
<!-- Markdown will be rendered here -->
</div>
</div>
<button class="preview-fold-btn" data-doc-id="{{ doc.id }}-list">Show more</button>
</div>
<div class="flex items-center mt-2 sm:mt-0">
<div class="document-actions">
<a href="{{ url_for('main.view_document', doc_id=doc.id) }}" class="action-btn" title="View">
<i class="mdi mdi-eye-outline"></i>
</a>
<a href="{{ url_for('main.edit_document', doc_id=doc.id) }}" class="action-btn" title="Edit">
<i class="mdi mdi-pencil-outline"></i>
</a>
<a href="{{ url_for('main.export_document', doc_id=doc.id) }}" class="action-btn" title="Export">
<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>
{% 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>
<!-- 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 %}
{% 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>
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
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');
});
});
// 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
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 %}