Files
TimeTracker/app/templates/comments/_comments_section.html
Dries Peeters ac19bebf2d feat: enhance offline sync and improve performance (v4.3.2)
- Add comprehensive offline sync improvements with enhanced IndexedDB support
- Optimize task model with cached total_hours calculation for better performance
- Improve task service query optimization and eager loading strategies
- Update CSP policy to allow CDN connections for improved resource loading
- Enhance service worker with better background sync capabilities
- Improve error handling and offline queue processing
- Update base template and comment templates for better UX
- Bump version to 4.3.2
2025-12-02 06:13:54 +01:00

183 lines
7.9 KiB
HTML

<!-- Comments section for projects and tasks -->
<div class="comments-section">
<div class="comments-header d-flex justify-content-between align-items-center mb-3">
<h6 class="mb-0">
<i class="fas fa-comments me-2 text-primary"></i>
{{ _('Comments') }}
{% if comments %}
<span class="badge bg-secondary ms-2">{{ comments|length }}</span>
{% endif %}
</h6>
<button type="button" class="btn btn-sm btn-primary" onclick="showNewCommentForm()">
<i class="fas fa-plus me-1"></i>{{ _('Add Comment') }}
</button>
</div>
<!-- New comment form (initially hidden) -->
<div class="new-comment-form d-none mb-4" id="new-comment-form">
<div class="card">
<div class="card-body">
<form method="POST" action="{{ url_for('comments.create_comment') }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
{% if project %}
<input type="hidden" name="project_id" value="{{ project.id }}">
{% elif task %}
<input type="hidden" name="task_id" value="{{ task.id }}">
{% endif %}
<div class="mb-3">
<label for="comment-content" class="form-label">{{ _('Your Comment') }}</label>
<textarea name="content" id="comment-content" class="form-control" rows="4"
placeholder="{{ _('Share your thoughts, updates, or questions...') }}" required></textarea>
<div class="form-text d-flex justify-content-between align-items-center">
<span>{{ _('You can use line breaks to format your comment.') }}</span>
<label class="btn btn-sm btn-outline-secondary mb-0">
<i class="fas fa-image me-1"></i>{{ _('Add Image') }}
<input type="file" id="commentImageInput" accept="image/*" class="d-none">
</label>
</div>
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">
<i class="fas fa-comment me-1"></i>{{ _('Post Comment') }}
</button>
<button type="button" class="btn btn-secondary" onclick="hideNewCommentForm()">
<i class="fas fa-times me-1"></i>{{ _('Cancel') }}
</button>
</div>
</form>
</div>
</div>
</div>
<!-- Comments list -->
<div class="comments-list">
{% if comments %}
{% for comment in comments %}
{% if not comment.parent_id %} {# Only show top-level comments, replies are handled in _comment.html #}
<div class="comment-item mb-4">
<div class="card">
<div class="card-body">
{% with depth=0 %}
{% include 'comments/_comment.html' %}
{% endwith %}
</div>
</div>
</div>
{% endif %}
{% endfor %}
{% else %}
<div class="no-comments text-center py-5">
<div class="text-muted mb-3">
<i class="fas fa-comments fa-3x opacity-50"></i>
</div>
<h6 class="text-muted mb-2">{{ _('No comments yet') }}</h6>
<p class="text-muted mb-3">{{ _('Start the conversation by adding the first comment.') }}</p>
<button type="button" class="btn btn-primary" onclick="showNewCommentForm()">
<i class="fas fa-plus me-1"></i>{{ _('Add First Comment') }}
</button>
</div>
{% endif %}
</div>
</div>
<script>
// Comments functionality
function showNewCommentForm() {
const form = document.getElementById('new-comment-form');
form.classList.remove('d-none');
document.getElementById('comment-content').focus();
}
function hideNewCommentForm() {
const form = document.getElementById('new-comment-form');
form.classList.add('d-none');
document.getElementById('comment-content').value = '';
}
function editComment(commentId) {
const content = document.getElementById('comment-content-' + commentId);
const editForm = document.getElementById('edit-form-' + commentId);
content.classList.add('d-none');
editForm.classList.remove('d-none');
editForm.querySelector('textarea').focus();
}
function cancelEdit(commentId) {
const content = document.getElementById('comment-content-' + commentId);
const editForm = document.getElementById('edit-form-' + commentId);
editForm.classList.add('d-none');
content.classList.remove('d-none');
}
function replyToComment(commentId) {
const replyForm = document.getElementById('reply-form-' + commentId);
replyForm.classList.remove('d-none');
replyForm.querySelector('textarea').focus();
}
function cancelReply(commentId) {
const replyForm = document.getElementById('reply-form-' + commentId);
replyForm.classList.add('d-none');
replyForm.querySelector('textarea').value = '';
}
function deleteComment(commentId) {
const commentElement = document.getElementById('comment-' + commentId);
const commentText = commentElement.querySelector('.comment-text').textContent;
// Show preview of comment to be deleted
document.getElementById('comment-preview').style.display = 'block';
document.getElementById('comment-preview-text').textContent = commentText.substring(0, 100) + (commentText.length > 100 ? '...' : '');
// Set form action
document.getElementById('deleteCommentForm').action = "{{ url_for('comments.delete_comment', comment_id=0) }}".replace('0', commentId);
// Show modal
new bootstrap.Modal(document.getElementById('deleteCommentModal')).show();
}
// Auto-resize textareas
document.addEventListener('DOMContentLoaded', function() {
const textareas = document.querySelectorAll('textarea');
textareas.forEach(function(textarea) {
textarea.addEventListener('input', function() {
this.style.height = 'auto';
this.style.height = (this.scrollHeight) + 'px';
});
});
// Add loading state to forms
const forms = document.querySelectorAll('.comments-section form');
forms.forEach(function(form) {
form.addEventListener('submit', function(e) {
const submitBtn = this.querySelector('button[type="submit"]');
const originalText = submitBtn.innerHTML;
submitBtn.innerHTML = '<div class="spinner-border spinner-border-sm me-2" role="status"></div>' + '{{ _('Saving...') }}';
submitBtn.disabled = true;
});
});
// Comment image upload
const imgInput = document.getElementById('commentImageInput');
if (imgInput) {
imgInput.addEventListener('change', async function(){
const file = this.files && this.files[0];
if (!file) return;
try {
const fd = new FormData(); fd.append('image', file, file.name || 'image.png');
const res = await fetch('{{ url_for('api.upload_editor_image') }}', { method:'POST', body: fd, credentials:'same-origin' });
const json = await res.json();
if (json && json.url) {
const ta = document.getElementById('comment-content');
const md = `![image](${json.url})`;
if (ta) { ta.value = (ta.value ? ta.value + "\n\n" : '') + md; ta.dispatchEvent(new Event('input')); }
}
} catch(e) { /* ignore */ }
finally { this.value = ''; }
});
}
});
</script>