This commit is contained in:
pika 2025-04-17 12:05:22 +02:00
parent 17885b005c
commit f5c8e9ee23
4 changed files with 756 additions and 44 deletions

View file

@ -51,6 +51,104 @@
-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 {
@ -91,6 +189,49 @@
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 %}
@ -108,7 +249,7 @@
<!-- 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
</button>
<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">
<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>
@ -158,9 +302,13 @@
{% endif %}
<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>
<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>
@ -220,20 +368,29 @@
</div>
<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>
<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">
<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 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>
@ -259,11 +416,130 @@
<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');
@ -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
const sidebarCategories = document.querySelectorAll('.category-item');
sidebarCategories.forEach(item => {